All Products
Search
Document Center

Vector Retrieval Service for Milvus:Build a local lifestyle leaderboard by combining geospatial search and AI Functions on Alibaba Cloud Milvus

Last Updated:Aug 13, 2026

Alibaba Cloud Milvus combines its GEOMETRY geospatial type with AI Functions so that a single search performs both geospatial range filtering and semantic recall over one Collection. This topic describes how to run "nearby" filtering and "on-taste" recall together, and then layer sentiment analysis and summary generation on top to build an explainable local lifestyle leaderboard.

Solution overview

"What is good to eat nearby" is the most frequent request in local lifestyle scenarios. To answer it well, a system must satisfy three conditions at the same time:

  • Nearby: stores within a given radius of the user's current location, or within the business district the user is in.

  • On taste: requests such as "a family-friendly Sichuan restaurant" or "a quiet cafe where I can talk business", which cannot be enumerated precisely with tags.

  • Trustworthy: what real reviews say, how high the positive-review rate is, and which strengths and complaints come up repeatedly.

    A traditional implementation usually stitches three systems together: a spatial database for geospatial filtering, a vector database for semantic recall, and an NLP service for sentiment classification and summarization. Each system keeps its own copy of the data, so cross-system joins, data synchronization, and consistency maintenance are expensive, and one query has to chain multiple services and several network round trips.

Dimension

Traditional solution (spatial database + vector database + NLP service)

Alibaba Cloud Milvus

Number of systems

3 or more, each with its own copy of the data

1, with a single source of data

Geospatial and semantic fusion

Cross-system joins or multiple recall rounds

One search(): ANN search + ST_ filtering

Review understanding

Self-built or external sentiment and summarization models

AI_SENTIMENT / AI_SUMMARIZE built in

Vectorization path

The application calls embedding first, then writes the data

Vectorized on insert

Consistency and synchronization

Maintained by yourself

Naturally consistent in a single database

Alibaba Cloud Milvus converges these capabilities into one Collection and one search:

Capability

Implementation

Geospatial range filtering

Store longitude and latitude in a GEOMETRY field, represent a business district as a POLYGON, and write spatial relationship functions such as ST_WITHIN into the filter expression at search time.

Semantic search over reviews

Review text is vectorized automatically at insert time by AI_EMBEDDING, and the search runs an ANN query against a query vector generated by the same model.

Relevance reranking

Recalled candidates are reordered by query relevance with AI_RERANK.

Reputation quantification

Use AI_SENTIMENT to classify review sentiment and aggregate the positive-review rate on the application side, and use AI_SUMMARIZE to compress a store's reviews into a short summary.

Geospatial filtering and semantic search run in a single search() call: the ANN vector search handles "on taste", the spatial functions in filter handle "nearby", both are solved jointly within one request, and reranking is applied on top to produce leaderboard candidates directly.

This topic uses the following AI Functions:

Function

What it does

Role in the leaderboard

How it is called

AI_EMBEDDING

Converts review text into vectors automatically at insert time, so the application does not need to call a model first.

Lets semantic requests such as "a family-friendly Sichuan restaurant" be matched by vector search.

Declared as a Function in the Collection schema (FunctionType.TEXTEMBEDDING).

AI_RERANK

Reorders recalled candidates by relevance to the query.

Moves the stores that best fit the request to the top of the leaderboard.

Attached as a ranker in search() (FunctionType.RERANK), or called through the REST endpoint /v2/vectordb/ai/rerank.

AI_SENTIMENT

Determines whether a review is positive, negative, or neutral.

Aggregates the positive-review rate of a store.

Called through the REST endpoint /v2/vectordb/ai/sentiment.

AI_SUMMARIZE

Compresses a large number of reviews into a short summary.

Generates the one-line comment shown in the leaderboard.

Called through the REST endpoint /v2/vectordb/ai/summarize.

AI_EMBEDDING and AI_RERANK can be declared in the Collection or attached to a search, while AI_SENTIMENT and AI_SUMMARIZE are available only through the REST interface.

Prerequisites

  • A Milvus 2.6 instance is created. The GEOMETRY type and AI Functions require the 2.6 kernel. After you create the instance, you do not need to bind a model service separately—declare the Function in the Collection directly.

  • To access the instance over the Internet, Public Network Access is enabled on the Security Configuration tab of the instance details page, and the client egress IP address is added to the public access whitelist.

  • pymilvus is installed. The examples in this topic are verified with pymilvus 3.0.0.

Limitations and usage notes

Review the following behaviors before you copy the code, because several of them produce silent or empty results rather than errors:

  • Geospatial boundary semantics — When a geometry object lies exactly on the boundary of a polygon, ST_WITHIN returns false and ST_INTERSECTS returns true. For example, when a radius circle is approximated by a polygon, a store located exactly on the circumference is not matched by ST_WITHIN. If your business logic must include the boundary, use ST_INTERSECTS instead. For a MULTIPOINT, ST_WITHIN returns false as soon as one of its points lies on the boundary.

  • Radius approximation accuracy — circle_to_wkt converts longitude and latitude with a planar approximation, so the error grows as the latitude increases. This method suits coarse radius filtering. If your business logic is sensitive to boundary precision, increase the num_seg segment count or verify the exact distance again on the application side.

  • REST interface port — The RESTful interface shares port 19530 with gRPC, so you must include the port explicitly in the address when you call the REST interface, for example http://c-xxx.milvus.aliyuncs.com:19530. If you omit the port, the request goes to port 80 by default and the connection times out.

  • REST rerank parameters — For the REST rerank interface, documents must be an array of strings and query must be a top-level string. The top_n parameter is not supported—passing it returns param [top_n] is not supported. The interface always returns one score per candidate, so if you need to truncate the list, sort by score and take the top N on the application side.

  • Undetermined sentiment — AI_SENTIMENT may return an empty value, which means the model could not determine the sentiment. Filter out empty values before you aggregate the positive-review rate, so that undetermined samples are not counted in the denominator.

Procedure

Run the steps in order within the same session. The code snippets form one continuous script: later steps reuse the client, helper functions, and variables that earlier steps define (for example, circle from Step 3 and QUERY and geo_filter from Step 4). If you copy a single step in isolation, define its upstream variables first.

Step 0: Prepare the shared code (required)

The following code contains the connection settings, a REST interface wrapper, and a utility function that approximates a radius circle as a polygon. All later steps depend on this code. Replace MILVUS_HOST and MILVUS_TOKEN with the information of your own instance.

from __future__ import annotations

import json
import math
import time
from typing import Any
from urllib.error import HTTPError
from urllib.request import Request, urlopen

from pymilvus import DataType, Function, FunctionType, MilvusClient

# ==================== Connection settings ====================
MILVUS_HOST = "http://c-xxx.milvus.aliyuncs.com"
MILVUS_TOKEN = "root:xxx"
# The RESTful interface shares port 19530 with gRPC. Include the port explicitly,
# otherwise the request goes to port 80 by default and the connection times out.
MILVUS_REST_BASE_URL = f"{MILVUS_HOST}:19530"

client = MilvusClient(uri=MILVUS_HOST, token=MILVUS_TOKEN)
collection_name = "shop_reviews"

def post_json(path: str, body: dict[str, Any], timeout: int = 120,
              retries: int = 3) -> tuple[int, dict[str, Any]]:
    """Shared wrapper for the AI_RERANK / AI_SENTIMENT / AI_SUMMARIZE REST interfaces."""
    last = None
    for _ in range(retries):
        request = Request(
            f"{MILVUS_REST_BASE_URL.rstrip('/')}{path}",
            data=json.dumps(body, ensure_ascii=False).encode("utf-8"),
            headers={"Authorization": f"Bearer {MILVUS_TOKEN}",
                     "Content-Type": "application/json"},
            method="POST",
        )
        try:
            with urlopen(request, timeout=timeout) as response:
                status, data = response.status, json.loads(response.read().decode("utf-8"))
        except HTTPError as exc:
            status, data = exc.code, json.loads(exc.read().decode("utf-8"))
        last = (status, data)
        if status == 200 and data.get("code") == 0:
            return status, data
        time.sleep(1)
    return last

def circle_to_wkt(lng: float, lat: float, radius_km: float, num_seg: int = 32) -> str:
    """Approximate the circle centered at (lng, lat) with a radius of radius_km as a regular polygon in WKT.
    Note: longitude and latitude are angles. This conversion is a planar approximation,
    so the error grows as the latitude increases."""
    d_lat = radius_km / 111.0
    d_lng = radius_km / (111.0 * math.cos(math.radians(lat)))
    pts = []
    for i in range(num_seg + 1):
        theta = 2 * math.pi * i / num_seg
        pts.append(f"{lng + d_lng * math.cos(theta):.6f} {lat + d_lat * math.sin(theta):.6f}")
    return f"POLYGON(({', '.join(pts)}))"

The MILVUS_HOST value is used as the client URI, while MILVUS_REST_BASE_URL appends port 19530 for the REST calls in later steps. The explicit-port requirement applies to the REST interface.

Step 1: Create a collection with a geospatial field and an embedding function

The location field uses DataType.GEOMETRY to store the coordinate point of a store. The review field carries an AI_EMBEDDING function that generates a 1,024-dimensional vector automatically at insert time. At the same time, create an HNSW index for the vector field and an R-Tree index for the geospatial field.

Warning

The following code runs drop_collection before it creates the Collection, which permanently deletes any existing Collection named shop_reviews and all of its data. On an instance that already holds business data, use a separate test Collection name or remove the drop_collection branch.

# ==================== Step 1: Create the collection ====================
if client.has_collection(collection_name):
    client.drop_collection(collection_name)

schema = MilvusClient.create_schema(auto_id=True, enable_dynamic_field=False)
schema.add_field("id", DataType.INT64, is_primary=True)
schema.add_field("shop_id", DataType.INT64)
schema.add_field("shop_name", DataType.VARCHAR, max_length=256)
schema.add_field("category", DataType.VARCHAR, max_length=64)
schema.add_field("location", DataType.GEOMETRY)                          # Store coordinates, WKT: POINT(lng lat)
schema.add_field("review", DataType.VARCHAR, max_length=4096)            # Raw review text, used as the embedding input
schema.add_field("review_embedding", DataType.FLOAT_VECTOR, dim=1024)    # Review vector

# Vectorize review automatically at insert time
schema.add_function(
    Function(
        name="embed_review",
        function_type=FunctionType.TEXTEMBEDDING,
        input_field_names=["review"],
        output_field_names=["review_embedding"],
        params={
            "provider": "aliyun_milvus",
            "model_name": "qwen3.7-text-embedding",
            "dim": 1024,
            "max_client_batch_size": 20,
            "max_concurrency": 1,
        },
    )
)

index_params = client.prepare_index_params()
index_params.add_index(
    field_name="review_embedding",
    index_type="HNSW",
    metric_type="COSINE",
    params={"M": 16, "efConstruction": 200},
)
# Create an R-Tree geospatial index for the GEOMETRY field to speed up spatial queries
index_params.add_index(
    field_name="location",
    index_type="RTREE",
)
client.create_collection(
    collection_name=collection_name,
    schema=schema,
    index_params=index_params,
)

The dimension of review_embedding must match the dim value in the Function parameters. Creating an RTREE index for the GEOMETRY field speeds up ST_ spatial queries and is recommended when the data volume is large.

The model_name values in this topic (qwen3.7-text-embedding here, and the rerank, sentiment, and summarization models in later steps) are examples. Replace each with a model that is available on your own instance.

Step 2: Insert store and review data

Insert the store coordinates and the raw review text. review_embedding is generated automatically by the Function.

# ==================== Step 2: Insert store and review data ====================
# review_embedding is generated automatically by the Function, so you do not pass vectors manually
rows = [
    {"shop_id": 1001, "shop_name": "Bashu Homestead Family Sichuan", "category": "Sichuan cuisine",
     "location": "POINT(116.4612 39.9088)",
     "review": "Came with my kid. High chairs and small bowls are available, the maoxuewang is not too spicy, the servers are patient, and the place is clean."},
    {"shop_id": 1001, "shop_name": "Bashu Homestead Family Sichuan", "category": "Sichuan cuisine",
     "location": "POINT(116.4612 39.9088)",
     "review": "You wait for a table on weekends, but the dishes are consistent. Good for a family meal, and my kid enjoyed it."},
    {"shop_id": 1002, "shop_name": "Mala Jianghu", "category": "Sichuan cuisine",
     "location": "POINT(116.4501 39.9101)",
     "review": "A paradise for bold flavors, the shuizhuyu is punchy, but the space is small and noisy, so it does not suit kids."},
    {"shop_id": 1003, "shop_name": "Quiet Lane Coffee", "category": "Coffee",
     "location": "POINT(116.4550 39.9075)",
     "review": "Quiet enough to talk business and the pour-over is good, but it has nothing to do with a Sichuan food request."},
]
client.insert(collection_name, rows)
client.flush(collection_name)
client.load_collection(collection_name)

Call flush() after inserting data, otherwise a search that runs immediately afterwards may return empty results. load_collection() loads the Collection into memory, which is required before you can search it.

Step 3: Filter by geospatial range

Choose the range method based on the shape of your target area:

  • Administrative district or business district — Represent the area as a POLYGON and use ST_WITHIN.

  • Area around the user's current location — Approximate a radius circle as a polygon with circle_to_wkt, then use ST_WITHIN.

  • Logic that must include the boundary or detect overlapping areas — Use ST_INTERSECTS instead, because a geometry located exactly on the boundary is not matched by ST_WITHIN.

    The following code shows both the business district polygon and the radius circle. The radius circle is approximated as a polygon by circle_to_wkt.

# ==================== Step 3: Geospatial range filtering ====================
# 3.1 Query the stores within a business district polygon
cbd_polygon = ("POLYGON((116.448 39.905, 116.465 39.905, 116.465 39.914, "
               "116.448 39.914, 116.448 39.905))")
hits = client.query(
    collection_name=collection_name,
    filter=f"ST_WITHIN(location, '{cbd_polygon}')",
    output_fields=["shop_name", "category", "location"],
    limit=100,
)
print("Review records within the business district:")
for h in hits:
    print(f"  - {h['shop_name']} ({h['category']})  {h['location']}")

# 3.2 Query within a 3 km radius: approximate the circle as a polygon, then check whether stores fall inside it
center_lng, center_lat = 116.4600, 39.9090
circle = circle_to_wkt(center_lng, center_lat, radius_km=3.0)
nearby = client.query(
    collection_name=collection_name,
    filter=f"ST_WITHIN(location, '{circle}')",
    output_fields=["shop_name", "category", "location"],
    limit=200,
)
print(f"Review records within 3 km of the center point ({center_lng}, {center_lat}):")
for n in nearby:
    print(f"  - {n['shop_name']} ({n['category']})  {n['location']}")

The sample data has 4 review records across 3 stores (Bashu Homestead has 2 reviews), so both queries return 4 review records. To output one row per store, deduplicate by shop_id on the application side.

Step 4: Combine semantic search with geospatial filtering

This step fuses "nearby" (the spatial functions in filter) and "on taste" (the ANN vector search) into a single search() call. It reuses circle from Step 3.

# ==================== Step 4: Combine semantic search with geospatial filtering ====================
# The ANN search over review vectors handles "on taste", the spatial functions in filter handle "nearby",
# and both are solved jointly in one request
QUERY = "family-friendly Sichuan restaurants nearby"
geo_filter = f"category == 'Sichuan cuisine' && ST_WITHIN(location, '{circle}')"
results = client.search(
    collection_name=collection_name,
    data=[QUERY],
    anns_field="review_embedding",
    filter=geo_filter,
    limit=20,
    output_fields=["shop_id", "shop_name", "category", "location", "review"],
)
print(f"Query: {QUERY}")
for rank, hit in enumerate(results[0], 1):
    e = hit["entity"]
    print(f"  {rank}. [similarity {hit['distance']:.4f}]  {e['shop_name']} | {e['review'][:24]}")

The following example shows the returned results. "Bashu Homestead Family Sichuan" ranks first because its reviews mention "high chairs" and "the servers are patient", which are semantically close to "family-friendly":

1. [similarity 0.6926]  Bashu Homestead Family Sichuan | Came with my kid. High c
2. [similarity 0.6591]  Mala Jianghu | A paradise for bold flav
3. [similarity 0.6091]  Bashu Homestead Family Sichuan | You wait for a table on

Step 5: Rerank candidates with AI_RERANK

Vector similarity reflects semantic closeness, which is not exactly the same as how relevant a result is to the query. Reranking the recalled candidates with AI_RERANK moves the stores that better fit the query intent to the front. This step reuses QUERY and geo_filter from Step 4.

Two options are available; choose one for your scenario:

  • Rerank within the retrieval pipeline — Attach a ranker in search() (used in 5.1). The model reranks the candidates that the ANN search recalls from the Collection.

  • Rerank an existing list of texts — Call the stateless REST interface /v2/vectordb/ai/rerank (used in 5.2). This path takes a text list directly and does not depend on a search.

    The two subsections below are alternatives, not sequential steps.

# ==================== Step 5: Rerank candidates with AI_RERANK ====================
RERANK_MODEL = "qwen3-rerank"

# 5.1 Attach a RERANK ranker in search(): after ANN recall, the model reranks the candidates
reranker = Function(
    name="rerank_reviews",
    function_type=FunctionType.RERANK,
    input_field_names=["review"],
    params={
        "reranker": "model",
        "provider": "aliyun_milvus",
        "model_name": RERANK_MODEL,
        "queries": [QUERY],
        "timeout_sec": 10,
    },
)
results = client.search(
    collection_name=collection_name,
    data=[QUERY],
    anns_field="review_embedding",
    filter=geo_filter,
    limit=20,
    output_fields=["shop_id", "shop_name", "review"],
    ranker=reranker,
)
for rank, hit in enumerate(results[0], 1):
    e = hit["entity"]
    print(f"  {rank}. [rerank score {hit['distance']:.4f}]  {e['shop_name']} | {e['review'][:24]}")

# 5.2 You can also use the stateless REST interface /v2/vectordb/ai/rerank
rerank_docs = [
    "Came with my kid. High chairs and small bowls are available, the maoxuewang is not too spicy, the servers are patient, and the place is clean.",
    "A paradise for bold flavors, the shuizhuyu is punchy, but the space is small and noisy, so it does not suit kids.",
]
status, data = post_json(
    "/v2/vectordb/ai/rerank",
    {
        "model_name": RERANK_MODEL,
        "query": QUERY,
        "documents": rerank_docs,
        "params": {"timeout_sec": 10},
    },
)
assert status == 200 and data.get("code") == 0, data
ranked = sorted(data["data"]["output"]["results"],
                key=lambda x: x["relevance_score"], reverse=True)
for rank, item in enumerate(ranked, 1):
    print(f"  {rank}. [relevance {item['relevance_score']:.4f}]  {rerank_docs[item['index']][:28]}")

The following example shows the output after reranking. Both reviews of "Bashu Homestead Family Sichuan" are ranked ahead of "Mala Jianghu":

1. [rerank score 0.6986]  Bashu Homestead Family Sichuan | Came with my kid. High c
2. [rerank score 0.5389]  Bashu Homestead Family Sichuan | You wait for a table on
3. [rerank score 0.5214]  Mala Jianghu | A paradise for bold flav

The REST interface returns its results in data.output.results, where each item corresponds to one candidate and contains index (the position of the candidate in the documents request parameter) and relevance_score (the relevance score). Sorting by score in descending order gives the reranked result:

1. [relevance 0.6986]  Came with my kid. High chairs
2. [relevance 0.5214]  A paradise for bold flavors,

Candidate text must cover every semantic dimension of the query. The query in this example, "family-friendly Sichuan restaurants nearby", covers both the "family-friendly" dimension and the "Sichuan cuisine" dimension, and a candidate that misses either one loses a significant amount of score. Take the first candidate as an example:

Candidate text

Relevance score

Came with my kid. High chairs and small bowls are available, the servers are patient. (no Sichuan cuisine information)

0.4723

Came with my kid. High chairs and small bowls are available, the maoxuewang is not too spicy, the servers are patient.

0.6666

Came with my kid. High chairs and small bowls are available, the maoxuewang is not too spicy, the servers are patient, and the place is clean.

0.6986

Adding a single Sichuan dish name raises the score from 0.4723 to 0.6666. By contrast, "A paradise for bold flavors, the shuizhuyu is punchy ... it does not suit kids." scores on the Sichuan cuisine dimension because it mentions "shuizhuyu", and its relevance stays at 0.5214. So if the first candidate had no Sichuan cuisine information, it would instead be ranked lower.

In production, therefore, send the full original text as the candidate for reranking, and do not trim away key information that may carry relevance, such as dish names or category words, just to shorten the text. When you evaluate reranking quality, also use the same full text that you use online.

Step 6: Compute the positive-review rate and generate review summaries

Use AI_SENTIMENT to classify the sentiment of all reviews of a store and aggregate the positive-review rate on the application side. Then use AI_SUMMARIZE to compress the reviews into a one-line comment. This step reads the data inserted in Step 2.

# ==================== Step 6: Sentiment analysis for the positive-review rate + summary generation ====================
shop_id = 1001
reviews = [r["review"] for r in client.query(
    collection_name=collection_name,
    filter=f"shop_id == {shop_id}",
    output_fields=["review"],
    limit=1000,
)]

# 6.1 Classify sentiment and aggregate the positive-review rate
status, sent = post_json("/v2/vectordb/ai/sentiment", {
    "model_name": "qwen3.7-max",
    "texts": reviews,
    "params": {"categories": ["positive", "negative", "neutral"], "temperature": 0},
})
assert status == 200 and sent.get("code") == 0, sent
labels = sent["data"]["output"]["outputs"]
valid = [x for x in labels if x]              # Empty values count as undetermined and are excluded from the aggregation
positive = sum(1 for x in valid if x == "positive")
good_rate = positive / len(valid) if valid else 0.0
print(f"Positive-review rate: {good_rate:.0%}   (valid samples {len(valid)}/{len(labels)})")

# 6.2 Generate the review summary of the store
status, summ = post_json("/v2/vectordb/ai/summarize", {
    "model_name": "qwen3.7-max",
    "texts": ["\n".join(reviews)],
    "params": {"max_words": 40, "temperature": 0,
               "prompt": "Summarize the overall reviews of this store in English, highlighting the signature dishes, the diners it suits, and the atmosphere."},
})
assert status == 200 and summ.get("code") == 0, summ
print(f"Store summary: {summ['data']['output']['outputs'][0]}")

Take shop_id 1001 as an example: both reviews are classified as positive, the positive-review rate is 100%, and the generated summary is "The place is clean and the maoxuewang is mild and palatable. The service is patient and children's tableware is available, so it suits family meals with kids, and you wait for a table on weekends."

The aggregation matches each label against the values you pass in the categories request parameter. AI_SENTIMENT may also return an empty value when the model cannot determine the sentiment; filter out empty values before you aggregate, as shown by the valid list.

Geometry types and spatial functions

This section describes the GEOMETRY type and the ST_ spatial relationship functions used in the procedure.

GEOMETRY and WKT

If you store longitude and latitude as two DOUBLE fields, the database treats them as two numbers and cannot determine "which one is inside which" or "whether two areas intersect". GEOMETRY is the geospatial type provided by Milvus 2.6. It stores points, lines, and polygons as geometry objects, which enables spatial relationship operations and lets a geospatial index accelerate queries.

Values of a GEOMETRY field use the WKT (Well-Known Text) format. The common types are as follows:

WKT type

Example

Meaning

Counterpart in this topic

POINT

POINT(116.4612 39.9088)

A single point

The coordinates of one store

LINESTRING

LINESTRING(116.46 39.90, 116.47 39.91)

A polyline

A road segment or a delivery route

POLYGON

POLYGON((116.448 39.905, ..., 116.448 39.905))

A closed area whose first and last coordinates are identical

A business district or a delivery area

MULTIPOINT

MULTIPOINT((116.46 39.90), (116.47 39.91))

Multiple points

Multiple stores of one brand

MULTIPOLYGON

MULTIPOLYGON(((...)),((...)))

Multiple areas

Multiple administrative districts

Coordinates are ordered longitude first and latitude second, that is POINT(lng lat), which is the reverse of the spoken order "latitude ... longitude ...". All coordinates in this topic use the WGS84 coordinate system.

ST_ spatial relationship functions

Functions that start with ST_ determine the spatial relationship between two geometry objects. You write them in a filter expression and they return true or false. This topic mainly uses the following three:

Function

Description

Use in this topic

ST_WITHIN(A, B)

Whether A lies entirely inside B.

Determines whether a store is inside a business district polygon, or within a given radius.

ST_CONTAINS(A, B)

Whether A entirely contains B, the inverse of ST_WITHIN.

Determines whether a business district contains a given store, for attribution analysis.

ST_INTERSECTS(A, B)

Whether A and B have any intersection or contact.

Determines whether a store's delivery area intersects the grid cell the user is in.

The following functions are also supported and can be used as needed:

Function

Description

Scenario

ST_EQUALS

Two geometry objects are identical.

Geometry deduplication.

ST_TOUCHES

Only the boundaries touch and the interiors do not overlap.

Determines whether adjacent business districts border each other.

ST_OVERLAPS

Partial overlap without containment.

Determines whether delivery areas cross.

ST_CROSSES

Crossing intersection.

Determines whether a road passes through a business district.

Leaderboard ranking logic

Chaining the preceding steps gives one complete leaderboard generation flow: geospatial filtering narrows the candidates down to "nearby", semantic search matches "on taste", reranking adjusts the order, and sentiment plus summarization fill in the "reputation". To produce one row per store, deduplicate the candidates by shop_id on the application side.

The final position can fuse three signals:

  • Geospatial signal: whether a store falls within the radius or the business district acts as the admission threshold, and stores that do not qualify are filtered out directly. Qualified stores then get a base weight according to distance.

  • Semantic relevance: the relevance_score returned by AI_RERANK, which measures how well a store's reviews fit the user's request.

  • Reputation: the positive-review rate aggregated by AI_SENTIMENT.

    You can combine these signals with a weighted fusion in the form of w1 · relevance score + w2 · positive-review rate + w3 · distance decay, and tune the weights to your business goals. Under this fusion, a store can rank high overall because it strongly matches the "family-friendly" semantics and because that match is combined with a high positive-review rate and a shorter distance, rather than by rating alone.

Further extensions

  • Multi-modal store search: use qwen3-vl-embedding to vectorize store photos and dish photos to support image-based store search such as "a nearby place with this kind of decor", which can also be fused with geospatial filtering in a single search.

  • Offline precomputation of reputation: when a store has a large number of reviews, use batch methods such as AI_BATCH to precompute the positive-review rate and the review summary offline and persist them, so that leaderboard queries only read the results, which balances cost and latency.

  • Richer spatial relationships: use ST_INTERSECTS to determine whether a store's delivery area intersects the grid cell the user is in, and use ST_CONTAINS for business district attribution analysis, extending the "geospatial + AI" fusion from finding stores to dispatching, site selection, and other steps.