Stay organized with collections
Save and categorize content based on your preferences.
This page describes how to rank search results for
full-text searches in
Spanner.
Spanner supports computing a topicality score, which provides a
building block for creating sophisticated ranking functions. These scores
calculate the relevance of a result to a query, based on the query term
frequency and other customizable options.
The following example shows how to perform a ranked search using the
SCORE
function:
The SCORE
function computes a score for each query term and then combines the scores. The
per-term score is roughly based on term frequency–inverse document frequency
(TF/IDF). The score is one
component of the final ordering for a record. The query combines it with other
signals, such as the freshness modulating the topicality score.
In the current implementation, the IDF part of TF/IDF is only available when
enhance_query=>true is used. It calculates the relative frequency of words
based on the full web corpus used by Google Search, rather than a
specific search index. If rquery enhancement isn't enabled, the scoring only
uses the term frequency (TF) component (that is, the IDF term is set to 1).
The SCORE function returns values that serve as relevance scores that
Spanner uses to establish a sort order. They have no standalone
meaning. The higher the score, the better it matches the query.
Usually arguments like query and enhance_query are the same across both
SEARCH and SCORE functions to ensure consistency in retrieval and ranking.
The recommended way to do this is to use these arguments with
query parameters
rather than string literals and specify the same query parameters in the
SEARCH and SCORE functions.
Score multiple columns
Spanner uses the SCORE function to score each field
individually. The query then combines these individual scores together. A common
way of doing this is to sum up the individual scores and then boost them
according to user-provided field weights (which are provided using SQL query
parameters).
For example, the following query combines the output of two SCORE functions:
This example uses query parameters $1, $2, $3, $4, $5, and $6
which are bound to values specified for titlequery, studioquery,
titleweight, studioweight, grammyweight, and freshnessweight,
respectively.
Spanner applies a multiplicative boost to the output of the
SCORE function for values that contain the query terms in the same order that
they appear in the query. There are two versions of this boost: partial match
and exact match. A partial match boost is applied when:
The TOKENLIST contains all the original terms in the query.
The tokens are adjacent to one another, and in the same order as they appear
in the query.
There are certain special rules for conjunctions, negations, and phrases:
A query with a negation can't receive a partial match boost.
A query with a conjunction receives a boost if part of the conjunction
appears in the appropriate locations.
A query with a phrase receives a boost if the phrase appears in the
TOKENLIST, and the term to the left of the phrase in the query appears to
the left of the phrase in the TOKENLIST, and the same applies to the term
to the right of the phrase.
Spanner applies an exact match boost when all of the previous
rules are true, and the first and last tokens in the query are the first and
last tokens in the document.
Example document: Bridge Over Troubled Water
Query
Boost Applied
Bridge Troubled
no boost
Bridge Over - other water
no boost
Bridge (Over OR Troubled) Water
no boost
Bridge Over
partial boost
Bridge Over (Troubled OR Water)
partial boost
Bridge Over Troubled Water
exact boost
Bridge "Over Troubled" Water
exact boost
Bridge ("Over Troubled" OR missingterm) Water
exact boost
Limit retrieval depth
Search indexes often contain millions of documents. For queries where the
predicates have low selectivity, it's impractical to rank all the results.
Scoring queries usually have two limits:
Retrieval depth limit: the maximum number of rows to score.
Result set size limit: the maximum number of rows that the query should
return (typically the page size).
Queries can limit retrieval depth with SQL subqueries:
[[["Easy to understand","easyToUnderstand","thumb-up"],["Solved my problem","solvedMyProblem","thumb-up"],["Other","otherUp","thumb-up"]],[["Hard to understand","hardToUnderstand","thumb-down"],["Incorrect information or sample code","incorrectInformationOrSampleCode","thumb-down"],["Missing the information/samples I need","missingTheInformationSamplesINeed","thumb-down"],["Other","otherDown","thumb-down"]],["Last updated 2025-08-07 UTC."],[],[],null,["# Rank search results\n\n| **Note:** This feature is available with the Spanner Enterprise edition and Enterprise Plus edition. For more information, see the [Spanner editions overview](/spanner/docs/editions-overview).\n\n\u003cbr /\u003e\n\nThis page describes how to rank search results for\n[full-text searches](/spanner/docs/full-text-search) in\nSpanner.\n\nSpanner supports computing a topicality score, which provides a\nbuilding block for creating sophisticated ranking functions. These scores\ncalculate the relevance of a result to a query, based on the query term\nfrequency and other customizable options.\n\nThe following example shows how to perform a ranked search using the\n[`SCORE`](/spanner/docs/reference/standard-sql/search_functions#score)\nfunction: \n\n### GoogleSQL\n\n SELECT AlbumId\n FROM Albums\n WHERE SEARCH(AlbumTitle_Tokens, \"fifth symphony\")\n ORDER BY SCORE(AlbumTitle_Tokens, \"fifth symphony\") DESC\n\n### PostgreSQL\n\nThis example uses [`spanner.search`](/spanner/docs/reference/postgresql/functions-and-operators#search_functions)\nwith\n[`spanner.score`](/spanner/docs/reference/postgresql/functions-and-operators#search_functions). \n\n SELECT albumid\n FROM albums\n WHERE spanner.search(albumtitle_tokens, 'fifth symphony')\n ORDER BY spanner.score(albumtitle_tokens, 'fifth symphony') DESC\n\nScore query terms with the `SCORE` function\n-------------------------------------------\n\nThe [`SCORE`](/spanner/docs/reference/standard-sql/search_functions#score)\nfunction computes a score for each query term and then combines the scores. The\nper-term score is roughly based on [term frequency--inverse document frequency\n(TF/IDF)](https://en.wikipedia.org/wiki/Tf%E2%80%93idf). The score is one\ncomponent of the final ordering for a record. The query combines it with other\nsignals, such as the freshness modulating the topicality score.\n\nIn the current implementation, the IDF part of TF/IDF is only available when\n`enhance_query=\u003etrue` is used. It calculates the relative frequency of words\nbased on the full web corpus used by Google Search, rather than a\nspecific search index. If rquery enhancement isn't enabled, the scoring only\nuses the term frequency (TF) component (that is, the IDF term is set to 1).\n\nThe `SCORE` function returns values that serve as relevance scores that\nSpanner uses to establish a sort order. They have no standalone\nmeaning. The higher the score, the better it matches the query.\n\nUsually arguments like `query` and `enhance_query` are the same across both\n`SEARCH` and `SCORE` functions to ensure consistency in retrieval and ranking.\n\nThe recommended way to do this is to use these arguments with\n[query parameters](/spanner/docs/reference/standard-sql/lexical#query_parameters)\nrather than string literals and specify the same query parameters in the\n`SEARCH` and `SCORE` functions.\n\nScore multiple columns\n----------------------\n\nSpanner uses the `SCORE` function to score each field\nindividually. The query then combines these individual scores together. A common\nway of doing this is to sum up the individual scores and then boost them\naccording to user-provided field weights (which are provided using SQL query\nparameters).\n\nFor example, the following query combines the output of two `SCORE` functions: \n\n### GoogleSQL\n\n SELECT AlbumId\n FROM Albums\n WHERE SEARCH(Title_Tokens, @p1) AND SEARCH(Studio_Tokens, @p2)\n ORDER BY SCORE(Title_Tokens, @p1) * @titleweight + SCORE(Studio_Tokens, @p2) * @studioweight\n LIMIT 25\n\n### PostgreSQL\n\nThis example uses query parameters `$1` and `$2` which are bound to\n'fifth symphony' and 'blue note', respectively. \n\n SELECT albumid\n FROM albums\n WHERE spanner.search(title_tokens, $1) AND spanner.search(studio_tokens, $2)\n ORDER BY spanner.score(title_tokens, $1) * $titleweight\n + spanner.score(studio_tokens, $2) * $studioweight\n LIMIT 25\n\nThe following example adds two boost parameters:\n\n- Freshness (`FreshnessBoost`) increases the score with `(1 + @freshnessweight * GREATEST(0, 30 - DaysOld) / 30)`\n- Popularity(`PopularityBoost`) increases the score by multiplying it by factor `(1 + IF(HasGrammy, @grammyweight, 0)`.\n\nFor readability, the query uses the `WITH` operator. \n\n### GoogleSQL\n\n SELECT AlbumId\n FROM Albums\n WHERE SEARCH(Title_Tokens, @p1) AND SEARCH(Studio_Tokens, @p2)\n ORDER BY WITH(\n TitleScore AS SCORE(Title_Tokens, @p1) * @titleweight,\n StudioScore AS SCORE(Studio_Tokens, @p2) * @studioweight,\n DaysOld AS (UNIX_MICROS(CURRENT_TIMESTAMP()) - ReleaseTimestamp) / 8.64e+10,\n FreshnessBoost AS (1 + @freshnessweight * GREATEST(0, 30 - DaysOld) / 30),\n PopularityBoost AS (1 + IF(HasGrammy, @grammyweight, 0)),\n (TitleScore + StudioScore) * FreshnessBoost * PopularityBoost)\n LIMIT 25\n\n### PostgreSQL\n\nThis example uses query parameters `$1`, `$2`, `$3`, `$4`, `$5`, and `$6`\nwhich are bound to values specified for `titlequery`, `studioquery`,\n`titleweight`, `studioweight`, `grammyweight`, and `freshnessweight`,\nrespectively. \n\n SELECT albumid\n FROM\n (\n SELECT\n albumid,\n spanner.score(title_tokens, $1) * $3 AS titlescore,\n spanner.score(studio_tokens, $2) * $4 AS studioscore,\n (extract(epoch FROM current_timestamp) * 10e+6 - releasetimestamp) / 8.64e+10 AS daysold,\n (1 + CASE WHEN hasgrammy THEN $5 ELSE 0 END) AS popularityboost\n FROM albums\n WHERE spanner.search(title_tokens, $1) AND spanner.search(studio_tokens, $2)\n ) AS subquery\n ORDER BY (subquery.TitleScore + subquery.studioscore)\n * (1 + $6 * greatest(0, 30 - subquery.daysold) / 30) * subquery.popularityboost\n LIMIT 25\n\n[`TOKENLIST_CONCAT`](/spanner/docs/reference/standard-sql/search_functions#tokenlist_concat)\ncan also used in both searching and scoring to simplify queries when\nappropriate. \n\n### GoogleSQL\n\n SELECT AlbumId\n FROM Albums\n WHERE SEARCH(TOKENLIST_CONCAT([Title_Tokens, Studio_Tokens]), @p)\n ORDER BY SCORE(TOKENLIST_CONCAT([Title_Tokens, Studio_Tokens]), @p)\n LIMIT 25\n\n### PostgreSQL\n\nThis example uses\n[`spanner.tokenlist_concat`](/spanner/docs/reference/postgresql/functions-and-operators#search_functions).\nThe query parameter `$1` is bound to 'blue note'. \n\n SELECT albumid\n FROM albums\n WHERE spanner.search(spanner.tokenlist_concat(ARRAY[title_tokens, studio_tokens]), $1)\n ORDER BY spanner.score(spanner.tokenlist_concat(ARRAY[title_tokens, studio_tokens]), $1)\n LIMIT 25\n\nBoost query order matches\n-------------------------\n\nSpanner applies a multiplicative boost to the output of the\n`SCORE` function for values that contain the query terms in the same order that\nthey appear in the query. There are two versions of this boost: partial match\nand exact match. A partial match boost is applied when:\n\n1. The `TOKENLIST` contains all the original terms in the query.\n2. The tokens are adjacent to one another, and in the same order as they appear in the query.\n\nThere are certain special rules for conjunctions, negations, and phrases:\n\n- A query with a negation can't receive a partial match boost.\n- A query with a conjunction receives a boost if part of the conjunction appears in the appropriate locations.\n- A query with a phrase receives a boost if the phrase appears in the `TOKENLIST`, and the term to the left of the phrase in the query appears to the left of the phrase in the `TOKENLIST`, and the same applies to the term to the right of the phrase.\n\nSpanner applies an exact match boost when all of the previous\nrules are true, and the first and last tokens in the query are the first and\nlast tokens in the document.\n\n**Example document: Bridge Over Troubled Water**\n\nLimit retrieval depth\n---------------------\n\nSearch indexes often contain millions of documents. For queries where the\npredicates have low selectivity, it's impractical to rank all the results.\nScoring queries usually have two limits:\n\n1. **Retrieval depth limit**: the maximum number of rows to score.\n2. **Result set size limit**: the maximum number of rows that the query should return (typically the page size).\n\nQueries can limit retrieval depth with SQL subqueries: \n\n### GoogleSQL\n\n SELECT *\n FROM (\n SELECT AlbumId, Title_Tokens\n FROM Albums\n WHERE SEARCH(Title_Tokens, @p1)\n ORDER BY ReleaseTimestamp DESC\n LIMIT @retrieval_limit\n )\n ORDER BY SCORE(Title_Tokens, @p1)\n LIMIT @page_size\n\n### PostgreSQL\n\nThis example uses query parameters `$1`, `$2`, and `$3` which are bound to\nvalues specified for `title_query`, `retrieval_limit`, and `page_size`,\nrespectively. \n\n SELECT *\n FROM (\n SELECT albumid, title_tokens\n FROM albums\n WHERE spanner.search(title_tokens, $1)\n ORDER BY releasetimestamp DESC\n LIMIT $2\n ) AS subquery\n ORDER BY spanner.score(subquery.title_tokens, $1)\n LIMIT $3\n\nThis works particularly well if Spanner uses the most important\nranking signal to sort the index.\n\nWhat's next\n-----------\n\n- Learn about [full-text search queries](/spanner/docs/full-text-search/query-overview).\n- Learn how to [perform a substring search](/spanner/docs/full-text-search/substring-search).\n- Learn how to [paginate search results](/spanner/docs/full-text-search/paginate-search-results).\n- Learn how to [mix full-text and non-text queries](/spanner/docs/full-text-search/mix-full-text-and-non-text-queries).\n- Learn how to [search multiple columns](/spanner/docs/full-text-search/search-multiple-columns)."]]