mono/packages/ui/docs/search-ex.md
2026-03-21 20:18:25 +01:00

1079 lines
33 KiB
Markdown

# Full-Text Search and VFS Indexing Architecture
This document covers the end-to-end architecture for Full-Text Search (FTS) regarding the Virtual File System (VFS). It details the extraction of textual content from uploaded files all the way to performing concurrent WebSearch language processing algorithms native to PostgreSQL.
## 1. Indexing & Text Extraction Pipeline
When an administrator forces a VFS reindex with the `fullText` option, the server systematically walks the specified mount, performing strict checks to extract the file content safely into the database.
### File Indexing Sequence
```mermaid
sequenceDiagram
participant Admin as VFS Admin (UI)
participant UI as [StorageManager.tsx](../src/components/admin/StorageManager.tsx)
participant Route as [vfs-routes.ts](../server/src/products/storage/api/vfs-routes.ts)
participant VFS as [vfs.ts](../server/src/products/storage/api/vfs.ts)
participant FS as Local File System
participant DB as Supabase DB
Admin->>UI: Select "Index All" + Full Text
UI->>Route: POST /api/vfs/index/{mount}?fullText=true
Route->>VFS: handleVfsIndex()
loop Every File Found in Walk
VFS->>VFS: Check Type and Size (Limit: VFS_INDEX_MAX_FILE_SIZE_KB)
VFS->>VFS: Validate Extension against allowFullTextExtensions
opt Passes Filters
VFS->>FS: Stream start readingz
FS-->>VFS: Output Stream chunks
VFS->>VFS: Verify NOT Binary (No Null Bytes)
end
VFS->>DB: Upsert Node Data to vfs_index
Note right of DB: fts tsvector auto-updated<br/>by trigger/stored procedure
end
VFS-->>Route: Return Batched OK
Route-->>UI: OK
```
### Core Components
* **[StorageManager.tsx](../src/components/admin/StorageManager.tsx)**: UI providing the checkbox to trigger `fullText` parameter for reindexing.
* **[vfs.json](../server/config/vfs.json)**: Externalized configuration supporting `allowFullTextExtensions` defining valid target patterns (e.g. `md`, `cpp`).
* **[vfs.ts](../server/src/products/storage/api/vfs.ts)**: Contains the central mechanism inside `handleVfsIndex` that implements extraction via async pipeline strategies (filters and transformers) wrapped in race limits to block hanging operations.
* **[vfs_index.sql](../supabase/migrations/20260308120300_create_vfs_index.sql)**: Contains the table structure. Specifically, you can see `fts tsvector GENERATED ALWAYS AS (...)` mapped tightly to combine the `name`, `path`, and `content`.
---
## 2. Searching & WebSearch Concurrency
The main unified search bar queries `/api/search` which dispatches nested parallel logic depending on the internal components. For files, we natively blend strict regex string checks (`ilike`) and Deep WebSearch functionality (`textSearch`).
### File Search Sequence
```mermaid
sequenceDiagram
participant User
participant Router as [db-search.ts](../server/src/products/serving/db/db-search.ts)
participant VFS as [vfs.ts](../server/src/products/storage/api/vfs.ts)
participant Supabase
User->>Router: GET /api/search?q=Anycubic+Chiron+Marlin&type=files
Router->>VFS: searchAllSearchableVfs(query)
par Path Substring Match
VFS->>Supabase: ilike('path', '%Anycubic%') & ilike('path', '%Marlin%')
and FTS Content Match
VFS->>Supabase: .textSearch('fts', query, { type: 'websearch' })
Note right of Supabase: Translates unquoted words to "&" organically
end
Supabase-->>VFS: return [pathRes, ftsRes]
VFS->>VFS: Map.set(row.id, deduplicated)
VFS-->>Router: Combined INode[] (with deep metadata)
Router->>Router: Enrich & Filter Visibility (ACL)
Router-->>User: Final Feed Items (meta.url mapped)
```
### Core Components
* **[db-search.ts](../server/src/products/serving/db/db-search.ts)**: Coordinates overarching feed blending globally. It processes hit arrays from various pipelines in parallel (Pages, Posts, Files etc.).
* **[vfs.ts](../server/src/products/storage/api/vfs.ts)**: Inside `searchAllSearchableVfs`, we issue the actual `.textSearch` querying function to Supabase and cleanly map any internal SQL errors safely to our standard error logging stream.
* **[search-fts.e2e.test.ts](../server/src/products/serving/__tests__/search-fts.e2e.test.ts)**: Validates exactly this layer ensuring end-to-end integration and data propagation accurately hit native database FTS queries safely.
---
## 3. TODOs & Future Enhancements
* **Google Search Bot Indexability**: Investigate and implement strategies for how web crawlers (like Googlebot) can discover and index our dynamic search results. Potential solutions include:
* **Server-Side Rendering (SSR) of Search Pages**: Ensuring initial page loads return fully populated HTML for search queries if accessed directly via URL parameters.
* **Dynamic Sitemaps (`sitemap.xml`)**: Generating sitemap endpoints that list popular or pre-computed search queries to guide crawlers.
* **Internal Linking Structure**: Exposing curated search links or tags throughout the application surface area (e.g., related topics, tag clouds) that crawlers can follow.
* **Structured Data**: Injecting JSON-LD or schema.org metadata representing the search capability and search result pages.
# Full Text Search
How to use full text search in PostgreSQL.
Postgres has built-in functions to handle `Full Text Search` queries. This is like a "search engine" within Postgres.
## Preparation
For this guide we'll use the following example data:
| id | title | author | description |
| --- | ----------------------------------- | ---------------------- | ------------------------------------------------------------------ |
| 1 | The Poky Little Puppy | Janette Sebring Lowrey | Puppy is slower than other, bigger animals. |
| 2 | The Tale of Peter Rabbit | Beatrix Potter | Rabbit eats some vegetables. |
| 3 | Tootle | Gertrude Crampton | Little toy train has big dreams. |
| 4 | Green Eggs and Ham | Dr. Seuss | Sam has changing food preferences and eats unusually colored food. |
| 5 | Harry Potter and the Goblet of Fire | J.K. Rowling | Fourth year of school starts, big drama ensues. |
```sql
create table books (
id serial primary key,
title text,
author text,
description text
);
insert into books
(title, author, description)
values
(
'The Poky Little Puppy',
'Janette Sebring Lowrey',
'Puppy is slower than other, bigger animals.'
),
('The Tale of Peter Rabbit', 'Beatrix Potter', 'Rabbit eats some vegetables.'),
('Tootle', 'Gertrude Crampton', 'Little toy train has big dreams.'),
(
'Green Eggs and Ham',
'Dr. Seuss',
'Sam has changing food preferences and eats unusually colored food.'
),
(
'Harry Potter and the Goblet of Fire',
'J.K. Rowling',
'Fourth year of school starts, big drama ensues.'
);
```
## Usage
The functions we'll cover in this guide are:
### `to_tsvector()` [#to-tsvector]
Converts your data into searchable tokens. `to_tsvector()` stands for "to text search vector." For example:
```sql
select to_tsvector('green eggs and ham');
-- Returns 'egg':2 'green':1 'ham':4
```
Collectively these tokens are called a "document" which Postgres can use for comparisons.
### `to_tsquery()` [#to-tsquery]
Converts a query string into tokens to match. `to_tsquery()` stands for "to text search query."
This conversion step is important because we will want to "fuzzy match" on keywords.
For example if a user searches for `eggs`, and a column has the value `egg`, we probably still want to return a match.
Postgres provides several functions to create tsquery objects:
- **`to_tsquery()`** - Requires manual specification of operators (`&`, `|`, `!`)
- **`plainto_tsquery()`** - Converts plain text to an AND query: `plainto_tsquery('english', 'fat rats')``'fat' & 'rat'`
- **`phraseto_tsquery()`** - Creates phrase queries: `phraseto_tsquery('english', 'fat rats')``'fat' <-> 'rat'`
- **`websearch_to_tsquery()`** - Supports web search syntax with quotes, "or", and negation
### Match: `@@` [#match]
The `@@` symbol is the "match" symbol for Full Text Search. It returns any matches between a `to_tsvector` result and a `to_tsquery` result.
Take the following example:
```sql
select *
from books
where title = 'Harry';
```
```js
const { data, error } = await supabase.from('books').select().eq('title', 'Harry')
```
```dart
final result = await client
.from('books')
.select()
.eq('title', 'Harry');
```
```swift
let response = try await supabase.from("books")
.select()
.eq("title", value: "Harry")
.execute()
```
```kotlin
val data = supabase.from("books").select {
filter {
eq("title", "Harry")
}
}
```
```python
data = supabase.from_('books').select().eq('title', 'Harry').execute()
```
The equality symbol above (`=`) is very "strict" on what it matches. In a full text search context, we might want to find all "Harry Potter" books and so we can rewrite the
example above:
```sql
select *
from books
where to_tsvector(title) @@ to_tsquery('Harry');
```
```js
const { data, error } = await supabase.from('books').select().textSearch('title', `'Harry'`)
```
```dart
final result = await client
.from('books')
.select()
.textSearch('title', "'Harry'");
```
```swift
let response = try await supabase.from("books")
.select()
.textSearch("title", value: "'Harry'")
```
```kotlin
val data = supabase.from("books").select {
filter {
textSearch("title", "'Harry'", TextSearchType.NONE)
}
}
```
## Basic full text queries
### Search a single column
To find all `books` where the `description` contain the word `big`:
```sql
select
*
from
books
where
to_tsvector(description)
@@ to_tsquery('big');
```
```js
const { data, error } = await supabase.from('books').select().textSearch('description', `'big'`)
```
```dart
final result = await client
.from('books')
.select()
.textSearch('description', "'big'");
```
```swift
let response = await client.from("books")
.select()
.textSearch("description", value: "'big'")
.execute()
```
```kotlin
val data = supabase.from("books").select {
filter {
textSearch("description", "'big'", TextSearchType.NONE)
}
}
```
```python
data = supabase.from_('books').select().text_search('description', "'big'").execute()
```
| id | title | author | description |
| --- | ----------------------------------- | ----------------- | ----------------------------------------------- |
| 3 | Tootle | Gertrude Crampton | Little toy train has big dreams. |
| 5 | Harry Potter and the Goblet of Fire | J.K. Rowling | Fourth year of school starts, big drama ensues. |
### Search multiple columns
Right now there is no direct way to use JavaScript or Dart to search through multiple columns but you can do it by creating [computed columns](https://postgrest.org/en/stable/api.html#computed-virtual-columns) on the database.
To find all `books` where `description` or `title` contain the word `little`:
```sql
select
*
from
books
where
to_tsvector(description || ' ' || title) -- concat columns, but be sure to include a space to separate them!
@@ to_tsquery('little');
```
```sql
create function title_description(books) returns text as $$
select $1.title || ' ' || $1.description;
$$ language sql immutable;
```
```js
const { data, error } = await supabase
.from('books')
.select()
.textSearch('title_description', `little`)
```
```sql
create function title_description(books) returns text as $$
select $1.title || ' ' || $1.description;
$$ language sql immutable;
```
```dart
final result = await client
.from('books')
.select()
.textSearch('title_description', "little")
```
```sql
create function title_description(books) returns text as $$
select $1.title || ' ' || $1.description;
$$ language sql immutable;
```
```swift
let response = try await client
.from("books")
.select()
.textSearch("title_description", value: "little")
.execute()
```
```sql
create function title_description(books) returns text as $$
select $1.title || ' ' || $1.description;
$$ language sql immutable;
```
```kotlin
val data = supabase.from("books").select {
filter {
textSearch("title_description", "title", TextSearchType.NONE)
}
}
```
```sql
create function title_description(books) returns text as $$
select $1.title || ' ' || $1.description;
$$ language sql immutable;
```
```python
data = supabase.from_('books').select().text_search('title_description', "little").execute()
```
| id | title | author | description |
| --- | --------------------- | ---------------------- | ------------------------------------------- |
| 1 | The Poky Little Puppy | Janette Sebring Lowrey | Puppy is slower than other, bigger animals. |
| 3 | Tootle | Gertrude Crampton | Little toy train has big dreams. |
### Match all search words
To find all `books` where `description` contains BOTH of the words `little` and `big`, we can use the `&` symbol:
```sql
select
*
from
books
where
to_tsvector(description)
@@ to_tsquery('little & big'); -- use & for AND in the search query
```
```js
const { data, error } = await supabase
.from('books')
.select()
.textSearch('description', `'little' & 'big'`)
```
```dart
final result = await client
.from('books')
.select()
.textSearch('description', "'little' & 'big'");
```
```swift
let response = try await client
.from("books")
.select()
.textSearch("description", value: "'little' & 'big'");
.execute()
```
```kotlin
val data = supabase.from("books").select {
filter {
textSearch("description", "'title' & 'big'", TextSearchType.NONE)
}
}
```
```python
data = supabase.from_('books').select().text_search('description', "'little' & 'big'").execute()
```
| id | title | author | description |
| --- | ------ | ----------------- | -------------------------------- |
| 3 | Tootle | Gertrude Crampton | Little toy train has big dreams. |
### Match any search words
To find all `books` where `description` contain ANY of the words `little` or `big`, use the `|` symbol:
```sql
select
*
from
books
where
to_tsvector(description)
@@ to_tsquery('little | big'); -- use | for OR in the search query
```
```js
const { data, error } = await supabase
.from('books')
.select()
.textSearch('description', `'little' | 'big'`)
```
```dart
final result = await client
.from('books')
.select()
.textSearch('description', "'little' | 'big'");
```
```swift
let response = try await client
.from("books")
.select()
.textSearch("description", value: "'little' | 'big'")
.execute()
```
```kotlin
val data = supabase.from("books").select {
filter {
textSearch("description", "'title' | 'big'", TextSearchType.NONE)
}
}
```
```python
response = client.from_('books').select().text_search('description', "'little' | 'big'").execute()
```
| id | title | author | description |
| --- | --------------------- | ---------------------- | ------------------------------------------- |
| 1 | The Poky Little Puppy | Janette Sebring Lowrey | Puppy is slower than other, bigger animals. |
| 3 | Tootle | Gertrude Crampton | Little toy train has big dreams. |
Notice how searching for `big` includes results with the word `bigger` (or `biggest`, etc).
## Partial search
Partial search is particularly useful when you want to find matches on substrings within your data.
### Implementing partial search
You can use the `:*` syntax with `to_tsquery()`. Here's an example that searches for any book titles beginning with "Lit":
```sql
select title from books where to_tsvector(title) @@ to_tsquery('Lit:*');
```
### Extending functionality with RPC
To make the partial search functionality accessible through the API, you can wrap the search logic in a stored procedure.
After creating this function, you can invoke it from your application using the SDK for your platform. Here's an example:
```sql
create or replace function search_books_by_title_prefix(prefix text)
returns setof books AS $$
begin
return query
select * from books where to_tsvector('english', title) @@ to_tsquery(prefix || ':*');
end;
$$ language plpgsql;
```
```js
const { data, error } = await supabase.rpc('search_books_by_title_prefix', { prefix: 'Lit' })
```
```dart
final data = await supabase.rpc('search_books_by_title_prefix', params: { 'prefix': 'Lit' });
```
```swift
let response = try await supabase.rpc(
"search_books_by_title_prefix",
params: ["prefix": "Lit"]
)
.execute()
```
```kotlin
val rpcParams = mapOf("prefix" to "Lit")
val result = supabase.postgrest.rpc("search_books_by_title_prefix", rpcParams)
```
```python
data = client.rpc('search_books_by_title_prefix', { 'prefix': 'Lit' }).execute()
```
This function takes a prefix parameter and returns all books where the title contains a word starting with that prefix. The `:*` operator is used to denote a prefix match in the `to_tsquery()` function.
## Handling spaces in queries
When you want the search term to include a phrase or multiple words, you can concatenate words using a `+` as a placeholder for space:
```sql
select * from search_books_by_title_prefix('Little+Puppy');
```
## Web search syntax with `websearch_to_tsquery()` [#websearch-to-tsquery]
The `websearch_to_tsquery()` function provides an intuitive search syntax similar to popular web search engines, making it ideal for user-facing search interfaces.
### Basic usage
```sql
select *
from books
where to_tsvector(description) @@ websearch_to_tsquery('english', 'green eggs');
```
```js
const { data, error } = await supabase
.from('books')
.select()
.textSearch('description', 'green eggs', { type: 'websearch' })
```
### Quoted phrases
Use quotes to search for exact phrases:
```sql
select * from books
where to_tsvector(description || ' ' || title) @@ websearch_to_tsquery('english', '"Green Eggs"');
-- Matches documents containing "Green" immediately followed by "Eggs"
```
### OR searches
Use "or" (case-insensitive) to search for multiple terms:
```sql
select * from books
where to_tsvector(description) @@ websearch_to_tsquery('english', 'puppy or rabbit');
-- Matches documents containing either "puppy" OR "rabbit"
```
### Negation
Use a dash (-) to exclude terms:
```sql
select * from books
where to_tsvector(description) @@ websearch_to_tsquery('english', 'animal -rabbit');
-- Matches documents containing "animal" but NOT "rabbit"
```
### Complex queries
Combine multiple operators for sophisticated searches:
```sql
select * from books
where to_tsvector(description || ' ' || title) @@
websearch_to_tsquery('english', '"Harry Potter" or "Dr. Seuss" -vegetables');
-- Matches books by "Harry Potter" or "Dr. Seuss" but excludes those mentioning vegetables
```
## Creating indexes
Now that you have Full Text Search working, create an `index`. This allows Postgres to "build" the documents preemptively so that they
don't need to be created at the time we execute the query. This will make our queries much faster.
### Searchable columns
Let's create a new column `fts` inside the `books` table to store the searchable index of the `title` and `description` columns.
We can use a special feature of Postgres called
[Generated Columns](https://www.postgresql.org/docs/current/ddl-generated-columns.html)
to ensure that the index is updated any time the values in the `title` and `description` columns change.
```sql
alter table
books
add column
fts tsvector generated always as (to_tsvector('english', description || ' ' || title)) stored;
create index books_fts on books using gin (fts); -- generate the index
select id, fts
from books;
```
```
| id | fts |
| --- | --------------------------------------------------------------------------------------------------------------- |
| 1 | 'anim':7 'bigger':6 'littl':10 'poki':9 'puppi':1,11 'slower':3 |
| 2 | 'eat':2 'peter':8 'rabbit':1,9 'tale':6 'veget':4 |
| 3 | 'big':5 'dream':6 'littl':1 'tootl':7 'toy':2 'train':3 |
| 4 | 'chang':3 'color':9 'eat':7 'egg':12 'food':4,10 'green':11 'ham':14 'prefer':5 'sam':1 'unus':8 |
| 5 | 'big':6 'drama':7 'ensu':8 'fire':15 'fourth':1 'goblet':13 'harri':9 'potter':10 'school':4 'start':5 'year':2 |
```
### Search using the new column
Now that we've created and populated our index, we can search it using the same techniques as before:
```sql
select
*
from
books
where
fts @@ to_tsquery('little & big');
```
```js
const { data, error } = await supabase.from('books').select().textSearch('fts', `'little' & 'big'`)
```
```dart
final result = await client
.from('books')
.select()
.textSearch('fts', "'little' & 'big'");
```
```swift
let response = try await client
.from("books")
.select()
.textSearch("fts", value: "'little' & 'big'")
.execute()
```
```kotlin
val data = supabase.from("books").select {
filter {
textSearch("fts", "'title' & 'big'", TextSearchType.NONE)
}
}
```
```python
data = client.from_('books').select().text_search('fts', "'little' & 'big'").execute()
```
| id | title | author | description | fts |
| --- | ------ | ----------------- | -------------------------------- | ------------------------------------------------------- |
| 3 | Tootle | Gertrude Crampton | Little toy train has big dreams. | 'big':5 'dream':6 'littl':1 'tootl':7 'toy':2 'train':3 |
## Query operators
Visit [Postgres: Text Search Functions and Operators](https://www.postgresql.org/docs/current/functions-textsearch.html)
to learn about additional query operators you can use to do more advanced `full text queries`, such as:
### Proximity: `<->` [#proximity]
The proximity symbol is useful for searching for terms that are a certain "distance" apart.
For example, to find the phrase `big dreams`, where the a match for "big" is followed immediately by a match for "dreams":
```sql
select
*
from
books
where
to_tsvector(description) @@ to_tsquery('big <-> dreams');
```
```js
const { data, error } = await supabase
.from('books')
.select()
.textSearch('description', `'big' <-> 'dreams'`)
```
```dart
final result = await client
.from('books')
.select()
.textSearch('description', "'big' <-> 'dreams'");
```
```swift
let response = try await client
.from("books")
.select()
.textSearch("description", value: "'big' <-> 'dreams'")
.execute()
```
```kotlin
val data = supabase.from("books").select {
filter {
textSearch("description", "'big' <-> 'dreams'", TextSearchType.NONE)
}
}
```
```python
data = client.from_('books').select().text_search('description', "'big' <-> 'dreams'").execute()
```
We can also use the `<->` to find words within a certain distance of each other. For example to find `year` and `school` within 2 words of each other:
```sql
select
*
from
books
where
to_tsvector(description) @@ to_tsquery('year <2> school');
```
```js
const { data, error } = await supabase
.from('books')
.select()
.textSearch('description', `'year' <2> 'school'`)
```
```dart
final result = await client
.from('books')
.select()
.textSearch('description', "'year' <2> 'school'");
```
```swift
let response = try await supabase
.from("books")
.select()
.textSearch("description", value: "'year' <2> 'school'")
.execute()
```
```kotlin
val data = supabase.from("books").select {
filter {
textSearch("description", "'year' <2> 'school'", TextSearchType.NONE)
}
}
```
```python
data = client.from_('books').select().text_search('description', "'year' <2> 'school'").execute()
```
### Negation: `!` [#negation]
The negation symbol can be used to find phrases which _don't_ contain a search term.
For example, to find records that have the word `big` but not `little`:
```sql
select
*
from
books
where
to_tsvector(description) @@ to_tsquery('big & !little');
```
```js
const { data, error } = await supabase
.from('books')
.select()
.textSearch('description', `'big' & !'little'`)
```
```dart
final result = await client
.from('books')
.select()
.textSearch('description', "'big' & !'little'");
```
```swift
let response = try await client
.from("books")
.select()
.textSearch("description", value: "'big' & !'little'")
.execute()
```
```kotlin
val data = supabase.from("books").select {
filter {
textSearch("description", "'big' & !'little'", TextSearchType.NONE)
}
}
```
```python
data = client.from_('books').select().text_search('description', "'big' & !'little'").execute()
```
## Ranking search results [#ranking]
Postgres provides ranking functions to sort search results by relevance, helping you present the most relevant matches first. Since ranking functions need to be computed server-side, use RPC functions and generated columns.
### Creating a search function with ranking [#search-function-ranking]
First, create a Postgres function that handles search and ranking:
```sql
create or replace function search_books(search_query text)
returns table(id int, title text, description text, rank real) as $$
begin
return query
select
books.id,
books.title,
books.description,
ts_rank(to_tsvector('english', books.description), to_tsquery(search_query)) as rank
from books
where to_tsvector('english', books.description) @@ to_tsquery(search_query)
order by rank desc;
end;
$$ language plpgsql;
```
Now you can call this function from your client:
```js
const { data, error } = await supabase.rpc('search_books', { search_query: 'big' })
```
```dart
final result = await client
.rpc('search_books', params: { 'search_query': 'big' });
```
```python
data = client.rpc('search_books', { 'search_query': 'big' }).execute()
```
```sql
select * from search_books('big');
```
### Ranking with weighted columns [#weighted-ranking]
Postgres allows you to assign different importance levels to different parts of your documents using weight labels. This is especially useful when you want matches in certain fields (like titles) to rank higher than matches in other fields (like descriptions).
#### Understanding weight labels
Postgres uses four weight labels: **A**, **B**, **C**, and **D**, where:
- **A** = Highest importance (weight 1.0)
- **B** = High importance (weight 0.4)
- **C** = Medium importance (weight 0.2)
- **D** = Low importance (weight 0.1)
#### Creating weighted search columns
First, create a weighted tsvector column that gives titles higher priority than descriptions:
```sql
-- Add a weighted fts column
alter table books
add column fts_weighted tsvector
generated always as (
setweight(to_tsvector('english', title), 'A') ||
setweight(to_tsvector('english', description), 'B')
) stored;
-- Create index for the weighted column
create index books_fts_weighted on books using gin (fts_weighted);
```
Now create a search function that uses this weighted column:
```sql
create or replace function search_books_weighted(search_query text)
returns table(id int, title text, description text, rank real) as $$
begin
return query
select
books.id,
books.title,
books.description,
ts_rank(books.fts_weighted, to_tsquery(search_query)) as rank
from books
where books.fts_weighted @@ to_tsquery(search_query)
order by rank desc;
end;
$$ language plpgsql;
```
#### Custom weight arrays
You can also specify custom weights by providing a weight array to `ts_rank()`:
```sql
create or replace function search_books_custom_weights(search_query text)
returns table(id int, title text, description text, rank real) as $$
begin
return query
select
books.id,
books.title,
books.description,
ts_rank(
'{0.0, 0.2, 0.5, 1.0}'::real[], -- Custom weights {D, C, B, A}
books.fts_weighted,
to_tsquery(search_query)
) as rank
from books
where books.fts_weighted @@ to_tsquery(search_query)
order by rank desc;
end;
$$ language plpgsql;
```
This example uses custom weights where:
- A-labeled terms (titles) have maximum weight (1.0)
- B-labeled terms (descriptions) have medium weight (0.5)
- C-labeled terms have low weight (0.2)
- D-labeled terms are ignored (0.0)
#### Using the weighted search
```js
// Search with standard weighted ranking
const { data, error } = await supabase.rpc('search_books_weighted', { search_query: 'Harry' })
// Search with custom weights
const { data: customData, error: customError } = await supabase.rpc('search_books_custom_weights', {
search_query: 'Harry',
})
```
```python
# Search with standard weighted ranking
data = client.rpc('search_books_weighted', { 'search_query': 'Harry' }).execute()
# Search with custom weights
custom_data = client.rpc('search_books_custom_weights', { 'search_query': 'Harry' }).execute()
```
```sql
-- Standard weighted search
select * from search_books_weighted('Harry');
-- Custom weighted search
select * from search_books_custom_weights('Harry');
```
#### Practical example with results
Say you search for "Harry". With weighted columns:
1. **"Harry Potter and the Goblet of Fire"** (title match) gets weight A = 1.0
2. **Books mentioning "Harry" in description** get weight B = 0.4
This ensures that books with "Harry" in the title ranks significantly higher than books that only mention "Harry" in the description, providing more relevant search results for users.
### Using ranking with indexes [#ranking-with-indexes]
When using the `fts` column you created earlier, ranking becomes more efficient. Create a function that uses the indexed column:
```sql
create or replace function search_books_fts(search_query text)
returns table(id int, title text, description text, rank real) as $$
begin
return query
select
books.id,
books.title,
books.description,
ts_rank(books.fts, to_tsquery(search_query)) as rank
from books
where books.fts @@ to_tsquery(search_query)
order by rank desc;
end;
$$ language plpgsql;
```
```js
const { data, error } = await supabase.rpc('search_books_fts', { search_query: 'little & big' })
```
```dart
final result = await client
.rpc('search_books_fts', params: { 'search_query': 'little & big' });
```
```python
data = client.rpc('search_books_fts', { 'search_query': 'little & big' }).execute()
```
```sql
select * from search_books_fts('little & big');
```
### Using web search syntax with ranking [#websearch-ranking]
You can also create a function that combines `websearch_to_tsquery()` with ranking for user-friendly search:
```sql
create or replace function websearch_books(search_text text)
returns table(id int, title text, description text, rank real) as $$
begin
return query
select
books.id,
books.title,
books.description,
ts_rank(books.fts, websearch_to_tsquery('english', search_text)) as rank
from books
where books.fts @@ websearch_to_tsquery('english', search_text)
order by rank desc;
end;
$$ language plpgsql;
```
```js
// Support natural search syntax
const { data, error } = await supabase.rpc('websearch_books', {
search_text: '"little puppy" or train -vegetables',
})
```
```sql
select * from websearch_books('"little puppy" or train -vegetables');
```
## Resources
- [Postgres: Text Search Functions and Operators](https://www.postgresql.org/docs/12/functions-textsearch.html)