Все статьи

Поиск в веб-приложении: PostgreSQL Full-Text Search, Elasticsearch и OpenSearch

Полное руководство для Full Stack разработчика: React, FastAPI, PostgreSQL, Elasticsearch, OpenSearch, индексация, релевантность и масштабирование поиска в Production

24 минут чтения

Представим, что вы разрабатываете SaaS-приложение для управления проектами.

Пользователи создают задачи, документы, комментарии и рабочие пространства.

Через несколько месяцев в базе данных появляются десятки тысяч записей.

Пользователи хотят быстро находить нужную информацию.

Например:

  • Найти задачу по названию.

  • Найти документ по содержимому.

  • Искать по нескольким словам.

  • Фильтровать результаты по проекту.

  • Сортировать по дате и релевантности.

  • Получать подсказки при вводе.

  • Находить слова с разными окончаниями.

  • Исправлять опечатки.

  • Искать только внутри своей организации.

На первый взгляд задача кажется простой.

Можно написать SQL-запрос:

SELECT *

FROM tasks

WHERE title ILIKE '%report%';

Для небольшого количества данных такой подход может работать.

Но постепенно появляются проблемы.

Поиск становится медленнее.

Пользователи ожидают результатов с учётом релевантности.

Нужна поддержка нескольких языков.

Появляются фильтры, автодополнение и сложные поисковые запросы.

А самое главное — в Multi-Tenant SaaS нельзя показывать документы одной организации пользователям другой.

Именно поэтому архитектура поиска в веб-приложении требует отдельного проектирования.

В этой статье разберём, как реализовать поиск в React и FastAPI, сравним PostgreSQL Full-Text Search, Elasticsearch и OpenSearch, создадим поисковые индексы, рассмотрим ранжирование, Fuzzy Search, Autocomplete, синхронизацию данных и Production-инфраструктуру.


1. Что такое поисковая система веб-приложения

Application Search Engine — подсистема, которая позволяет пользователям находить данные по текстовым запросам и дополнительным условиям.

Например:

User enters query

|

v

React Search UI

|

v

FastAPI Search Endpoint

|

v

Search Engine

|

v

Search Results

Поисковая система может работать непосредственно с PostgreSQL или использовать отдельный поисковый движок.

Выбор зависит от объёма данных, сложности запросов и требований к скорости.

2. Чем поиск отличается от обычной фильтрации

Search vs Filtering — два связанных, но разных механизма.

Фильтрация выбирает записи по определённым условиям.

Например:

SELECT *

FROM tasks

WHERE status = 'open';

Поиск определяет, какие документы соответствуют текстовому запросу.

Например:

"annual financial report"

При этом результаты могут иметь разную степень релевантности.

Современный поисковый интерфейс обычно объединяет текстовый поиск и фильтрацию.

3. Какие виды поиска существуют

Types of Search in Web Applications:

Тип

Пример

Exact Match

Поиск точного значения

Prefix Search

Слова, начинающиеся с rep

Substring Search

Подстрока внутри текста

Full-Text Search

Поиск по словам и их формам

Fuzzy Search

Поиск с опечатками

Semantic Search

Поиск по смыслу

Faceted Search

Поиск с фильтрами и агрегатами

Autocomplete

Подсказки при вводе

Каждый тип решает определённую задачу.

Не существует одного поискового алгоритма, который одинаково хорошо подходит для всех сценариев.


4. Почему LIKE и ILIKE не всегда достаточно

SQL LIKE vs Full-Text Search:

SELECT *

FROM documents

WHERE content ILIKE '%database%';

Такой запрос ищет подстроку.

Он не учитывает лингвистические особенности текста.

Например, запрос:

running

не обязательно найдёт:

run

Кроме того, обычный B-tree индекс, как правило, не помогает эффективно выполнять поиск с ведущим %.

Но для небольших таблиц ILIKE может быть вполне приемлемым решением.

5. Когда стоит использовать PostgreSQL

PostgreSQL Search Engine хорошо подходит, если:

  • данные уже находятся в PostgreSQL;

  • объём поиска умеренный;

  • не требуется сложная поисковая аналитика;

  • важна простота инфраструктуры;

  • поиск тесно связан с транзакционными данными;

  • необходимы строгие проверки доступа.

PostgreSQL поддерживает встроенный Full-Text Search и дополнительные расширения.

Поэтому отдельный Elasticsearch-кластер не всегда нужен.

6. Когда нужен отдельный поисковый движок

Dedicated Search Engine Architecture становится полезной, если нужны:

  • сложное ранжирование;

  • большое количество текстовых документов;

  • распределённый поиск;

  • продвинутые агрегаты;

  • гибкие анализаторы;

  • сложный Autocomplete;

  • высокая поисковая нагрузка;

  • специализированные возможности поиска.

Но отдельный движок увеличивает операционную сложность.

Теперь данные нужно синхронизировать между основной базой и поисковым индексом.


PostgreSQL Full-Text Search

7. Что такое PostgreSQL Full-Text Search

PostgreSQL Full-Text Search — встроенный механизм поиска по текстовым документам.

Он использует специальные типы данных и функции.

Основные элементы:

  • tsvector

    ;

  • tsquery

    ;

  • to_tsvector

    ;

  • plainto_tsquery

    ;

  • websearch_to_tsquery

    ;

  • ts_rank

    ;

  • GIN Index.

Вместо обычного поиска подстроки PostgreSQL анализирует текст и формирует набор лексем.

8. Что такое tsvector

PostgreSQL tsvector — тип данных для хранения подготовленного текстового представления документа.

Пример:

SELECT to_tsvector(

'english',

'The developers are building applications'

);

Результат будет содержать нормализованные лексемы.

Точная форма результата зависит от используемой конфигурации текстового поиска.

9. Что такое tsquery

PostgreSQL tsquery представляет поисковый запрос.

Например:

SELECT plainto_tsquery(

'english',

'building applications'

);

PostgreSQL преобразует обычный текст в структуру, пригодную для полнотекстового поиска.

10. Как выполнить Full-Text Search

PostgreSQL Full Text Search Example:

SELECT *

FROM documents

WHERE to_tsvector(

'english',

content

) @@ plainto_tsquery(

'english',

'database optimization'

);

Оператор @@ проверяет соответствие документа поисковому запросу.

Но вычисление to_tsvector для большого количества строк может быть дорогим.

Поэтому для Production полезно использовать индекс.


11. Как создать GIN Index

GIN Index PostgreSQL Full Text Search:

CREATE INDEX idx_documents_search

ON documents

USING GIN (

to_tsvector(

'english',

content

)

);

GIN Index позволяет ускорять поиск по лексемам.

Но индекс увеличивает:

  • использование диска;

  • стоимость INSERT;

  • стоимость UPDATE;

  • нагрузку на обслуживание индекса.

Поэтому индексировать нужно действительно используемые поисковые поля.

12. Как хранить Search Vector

Generated tsvector Column PostgreSQL:

CREATE TABLE documents (

id UUID PRIMARY KEY,

organization_id UUID NOT NULL,

title TEXT NOT NULL,

content TEXT NOT NULL,

search_vector TSVECTOR GENERATED ALWAYS AS (

setweight(

to_tsvector(

'english',

coalesce(title, '')

),

'A'

) ||

setweight(

to_tsvector(

'english',

coalesce(content, '')

),

'B'

)

) STORED

);

Здесь:

  • Title получает вес

    A

    .

  • Content получает вес

    B

    .

Это позволяет учитывать важность разных частей документа.

13. Как индексировать Generated Column

PostgreSQL Search Vector Index:

CREATE INDEX idx_documents_search_vector

ON documents

USING GIN (search_vector);

Теперь поиск можно выполнять по заранее подготовленному Search Vector.

SELECT *

FROM documents

WHERE search_vector @@ plainto_tsquery(

'english',

'database optimization'

);

14. Как настроить релевантность

PostgreSQL ts_rank позволяет ранжировать документы.

Например:

SELECT

id,

title,

ts_rank(

search_vector,

plainto_tsquery(

'english',

'database optimization'

)

) AS rank

FROM documents

WHERE search_vector @@ plainto_tsquery(

'english',

'database optimization'

)

ORDER BY rank DESC;

Документы с более высоким Rank отображаются раньше.

Но Rank — не универсальная оценка качества результата.

Его необходимо проверять на реальных поисковых запросах.


15. Как реализовать поиск по нескольким полям

Weighted Full Text Search PostgreSQL позволяет задавать разные веса.

Например:

Title -> A

Description -> B

Content -> C

Tags -> D

Это полезно, если совпадение в названии должно быть важнее совпадения в основном тексте.

Но веса следует подбирать с учётом конкретного продукта.

16. Как искать фразы

Phrase Search PostgreSQL:

SELECT *

FROM documents

WHERE search_vector @@ phraseto_tsquery(

'english',

'database performance'

);

Phrase Search учитывает последовательность лексем и их позиции.

Но его поведение отличается от буквального поиска точной строки.

Для строгого поиска символ-в-символ могут понадобиться дополнительные условия.

17. Как использовать Web Search Syntax

websearch_to_tsquery PostgreSQL:

SELECT *

FROM documents

WHERE search_vector @@ websearch_to_tsquery(

'english',

'"database performance" OR optimization'

);

Эта функция поддерживает удобный синтаксис пользовательских запросов.

Например:

  • кавычки для фраз;

  • OR

    ;

  • исключение терминов через минус.

Это полезно для поисковой строки в интерфейсе.


18. Как реализовать поиск на русском языке

Russian Full Text Search PostgreSQL использует конфигурацию russian.

SELECT to_tsvector(

'russian',

'Разработчики создают современные приложения'

);

PostgreSQL применяет соответствующую лингвистическую обработку.

Но встроенный поиск не решает все проблемы русского языка.

Например, он не всегда корректно обрабатывает:

  • сложные названия;

  • аббревиатуры;

  • опечатки;

  • смешанные языки;

  • транслитерацию;

  • специфическую терминологию.

19. Как поддерживать несколько языков

Multilingual Full Text Search требует продуманной стратегии.

Возможные подходы:

  1. Хранить язык документа.

  2. Создавать Search Vector с соответствующей конфигурацией.

  3. Поддерживать несколько поисковых полей.

  4. Использовать отдельные анализаторы в Elasticsearch или OpenSearch.

Для многоязычного SaaS необходимо учитывать язык документа и язык поискового запроса.

20. Как реализовать поиск с опечатками

PostgreSQL Fuzzy Search pg_trgm — расширение для поиска похожих строк.

CREATE EXTENSION IF NOT EXISTS pg_trgm;

Пример:

SELECT

title,

similarity(title, 'databse') AS score

FROM documents

WHERE title % 'databse'

ORDER BY score DESC;

Оператор % использует порог сходства pg_trgm.

Этот подход полезен для названий, коротких текстов и поиска с небольшими опечатками.

21. Как ускорить Trigram Search

pg_trgm GIN Index:

CREATE INDEX idx_documents_title_trgm

ON documents

USING GIN (

title gin_trgm_ops

);

Такой индекс может ускорять некоторые запросы с LIKE, ILIKE и Trigram-операторами.

Но производительность зависит от структуры запроса и селективности условий.


Поиск в FastAPI

22. Как создать Search API

FastAPI Search API:

GET /api/search?q=database

В реальном приложении Endpoint может принимать:

GET /api/search?q=database&limit=20

Дополнительные параметры:

  • Cursor;

  • Project ID;

  • Document Type;

  • Sort;

  • Date Range.

Важно ограничивать размер пользовательского запроса.

23. Как реализовать Search Endpoint

FastAPI PostgreSQL Search:

from fastapi import FastAPI, Query

app = FastAPI()

@app.get("/api/search")

async def search_documents(

q: str = Query(

min_length=1,

max_length=200

),

limit: int = Query(

20,

ge=1,

le=100

),

):

return await search_service.search(

query=q,

limit=limit,

)

search_service — компонент приложения, который необходимо реализовать отдельно.

В Production необходимо добавить Authentication и Tenant Authorization.

24. Как безопасно передавать поисковый запрос

SQL Injection Prevention Search требует параметризованных SQL-запросов.

Нельзя собирать SQL через конкатенацию пользовательского ввода.

Плохой пример:

sql = f"""

SELECT *

FROM documents

WHERE title ILIKE '%{query}%'

"""

Лучше использовать параметры:

sql = """

SELECT *

FROM documents

WHERE search_vector @@

websearch_to_tsquery('english', :query)

LIMIT :limit

"""

И передавать значения через драйвер или ORM.

25. Как реализовать Multi-Tenant Search

Multi Tenant Search Security:

SELECT

id,

title

FROM documents

WHERE organization_id = :organization_id

AND search_vector @@ websearch_to_tsquery(

'english',

:query

)

ORDER BY

ts_rank(

search_vector,

websearch_to_tsquery(

'english',

:query

)

) DESC

LIMIT :limit;

organization_id должен быть получен из проверенного Tenant Context.

Нельзя доверять произвольному Organization ID от клиента.

26. Почему фильтр Tenant обязателен

Search Tenant Isolation — критическое требование B2B SaaS.

Например:

Organization A

|

+-- Document A

Organization B

|

+-- Document B

Пользователь Organization A не должен находить Document B.

Даже если название документа совпадает с его поисковым запросом.

Это касается не только основного списка результатов, но и Autocomplete, Counts, Facets и Highlighting.


Elasticsearch и OpenSearch

27. Что такое Elasticsearch

Elasticsearch Search Engine — распределённый поисковый движок, построенный вокруг Apache Lucene.

Он поддерживает:

  • Full-Text Search;

  • индексацию;

  • анализаторы;

  • релевантность;

  • агрегаты;

  • фильтры;

  • распределённое хранение;

  • различные поисковые сценарии.

Elasticsearch часто используется для поиска по большим коллекциям документов.

28. Что такое OpenSearch

OpenSearch Search Engine — поисковая и аналитическая платформа, также основанная на Apache Lucene.

OpenSearch возник как отдельный проект и развивается независимо от Elasticsearch.

Оба решения поддерживают многие схожие концепции.

Но API, возможности, плагины, лицензирование и совместимость могут различаться.

Поэтому нельзя считать их полностью взаимозаменяемыми во всех версиях.

29. Elasticsearch vs OpenSearch

Elasticsearch vs OpenSearch Comparison:

Критерий

Elasticsearch

OpenSearch

Основа

Apache Lucene

Apache Lucene

Full-Text Search

Да

Да

Distributed Search

Да

Да

Aggregations

Да

Да

REST API

Да

Да

Экосистема

Elastic

OpenSearch

Лицензирование

Зависит от версии и компонентов

Проект использует Apache 2.0

Совместимость

Зависит от версий

Зависит от версий

При выборе следует учитывать требования к функциям, лицензированию, эксплуатации и управляемым сервисам.

30. Как устроен Elasticsearch Index

Elasticsearch Index Architecture:

Elasticsearch Cluster

|

+-- Index: documents

| |

| +-- Shard 1

| |

| +-- Shard 2

|

+-- Index: tasks

Index содержит документы.

Документы состоят из полей.

Shards позволяют распределять данные.

Replicas могут использоваться для отказоустойчивости и масштабирования чтения.


31. Что такое Mapping

Elasticsearch Mapping определяет структуру полей.

Например:

{

"mappings": {

"properties": {

"organization_id": {

"type": "keyword"

},

"title": {

"type": "text"

},

"status": {

"type": "keyword"

},

"created_at": {

"type": "date"

}

}

}

}

Тип text подходит для полнотекстового поиска.

Тип keyword — для точных совпадений, фильтрации и некоторых видов агрегирования.

32. Почему Text и Keyword отличаются

Elasticsearch Text vs Keyword:

Text:

"Database Optimization"

|

v

Tokenization

|

v

database

optimization

Для keyword значение обычно сохраняется как единый термин.

Поэтому поля типа keyword часто используют для:

  • Tenant ID;

  • Status;

  • Category;

  • External ID;

  • Tags.

33. Что такое Analyzer

Elasticsearch Text Analyzer преобразует текст перед индексацией и поиском.

Analyzer может включать:

  • Character Filters;

  • Tokenizer;

  • Token Filters.

Например:

"The Quick Brown Fox"

|

v

Analyzer

|

v

quick

brown

fox

Конкретный результат зависит от конфигурации.

34. Как настроить русский Analyzer

Elasticsearch Russian Analyzer можно использовать для обработки русского текста.

Пример Mapping:

{

"mappings": {

"properties": {

"title": {

"type": "text",

"analyzer": "russian"

}

}

}

}

Встроенный Russian Analyzer поддерживает базовую языковую обработку.

Но для специфических терминов может потребоваться Custom Analyzer.


35. Как индексировать документ

Elasticsearch Document Indexing:

PUT /documents/_doc/123

Content-Type: application/json

{

"organization_id": "org_42",

"title": "Database Optimization",

"content": "How to optimize PostgreSQL queries",

"status": "published"

}

После индексации документ становится доступным для поиска после соответствующего Refresh.

Поиск в Elasticsearch обычно имеет Near Real-Time семантику.

Это означает, что только что записанный документ не всегда немедленно виден поисковому запросу.

36. Как выполнить Match Query

Elasticsearch Match Query:

{

"query": {

"match": {

"title": "database optimization"

}

}

}

Match Query анализирует текст запроса.

Затем Elasticsearch ищет подходящие документы.

37. Как выполнить Multi Match Query

Elasticsearch Multi Match Query:

{

"query": {

"multi_match": {

"query": "database optimization",

"fields": [

"title^3",

"content"

]

}

}

}

Здесь совпадение в title получает повышенный вес.

Но итоговое ранжирование зависит от Query, Analyzer и статистики индекса.


38. Как работает BM25

BM25 Search Relevance — алгоритм ранжирования, широко используемый в поисковых движках.

Он учитывает:

  • частоту терминов;

  • распространённость терминов среди документов;

  • длину документа;

  • параметры нормализации.

Например, документ с несколькими важными совпадениями может получить более высокий Score.

Но BM25 не понимает смысл текста так, как это делают Semantic Search модели.

39. Как улучшить релевантность

Search Relevance Optimization включает:

  • Field Boosting;

  • Phrase Matching;

  • анализаторы;

  • Synonyms;

  • Fuzzy Matching;

  • фильтрацию;

  • учёт свежести;

  • бизнес-сигналы.

Например, в системе задач можно повысить приоритет:

  • активных задач;

  • документов текущего проекта;

  • записей с точным совпадением названия.

Но бизнес-правила не должны нарушать Authorization.


40. Как реализовать фильтры

Elasticsearch Bool Query Filters:

{

"query": {

"bool": {

"must": [

{

"match": {

"title": "database"

}

}

],

"filter": [

{

"term": {

"organization_id": "org_42"

}

},

{

"term": {

"status": "published"

}

}

]

}

}

}

Фильтры помогают ограничивать результаты.

В Multi-Tenant SaaS Tenant Filter должен быть сформирован доверенной серверной логикой.

Но одного фильтра в поисковом запросе может быть недостаточно для чувствительных данных: при необходимости Backend должен повторно проверять права на конкретные документы.

41. Что такое Faceted Search

Faceted Search Elasticsearch — поиск с агрегированными фильтрами.

Например:

Search: "database"

Status:

[ ] Open (42)

[ ] Closed (18)

Priority:

[ ] High (12)

[ ] Medium (35)

[ ] Low (13)

Количество результатов по категориям можно вычислять через Aggregations.

Но Facet Counts тоже могут раскрывать информацию.

Поэтому они должны учитывать Tenant Isolation и остальные ограничения доступа.

42. Как создать Terms Aggregation

Elasticsearch Terms Aggregation:

{

"size": 0,

"aggs": {

"statuses": {

"terms": {

"field": "status"

}

}

}

}

Такой запрос возвращает агрегированные данные по значениям поля.

В реальном SaaS необходимо добавить соответствующие фильтры доступа.


Autocomplete и Fuzzy Search

43. Что такое Autocomplete

Autocomplete Search React — подсказки, появляющиеся во время ввода.

Например:

User types:

"post"

Suggestions:

PostgreSQL

PostgreSQL Indexes

PostgreSQL Performance

Autocomplete улучшает удобство поиска.

Но требует отдельной оптимизации.

Нельзя запускать тяжёлый Full-Text Search после каждого нажатия клавиши без ограничений.

44. Как реализовать Autocomplete в PostgreSQL

PostgreSQL Autocomplete Search:

SELECT id, title

FROM documents

WHERE organization_id = :organization_id

AND title ILIKE :prefix

ORDER BY title

LIMIT 10;

Здесь :prefix формируется на Backend как безопасный параметр, например post%.

Для эффективного Prefix Search можно рассмотреть соответствующий индекс с учётом Collation и структуры запроса.

45. Как реализовать Autocomplete в Elasticsearch

Elasticsearch Search As You Type может использовать тип поля search_as_you_type.

Например:

{

"mappings": {

"properties": {

"title": {

"type": "search_as_you_type"

}

}

}

}

Это один из вариантов реализации поиска по мере ввода.

Другие варианты включают Completion Suggester и Edge N-Grams.

Выбор зависит от требований к подсказкам.

46. Как реализовать Fuzzy Search

Elasticsearch Fuzzy Matching:

{

"query": {

"match": {

"title": {

"query": "databse",

"fuzziness": "AUTO"

}

}

}

}

Такой запрос может находить документы с небольшими ошибками в написании.

Но Fuzzy Matching увеличивает стоимость поиска.

Поэтому необходимо контролировать параметры и нагрузку.


React Search UI

47. Как построить поисковый интерфейс

React Search Component Architecture:

Search Page

|

+-- Search Input

|

+-- Autocomplete

|

+-- Filters

|

+-- Results List

|

+-- Pagination

Каждый компонент должен иметь отдельную ответственность.

Например, Search Input управляет вводом, а Results List отображает результаты API.

48. Как реализовать Search Input

React Search Input TypeScript:

import { useState } from "react";

export function SearchInput() {

const [query, setQuery] = useState("");

return (

<input

type="search"

value={query}

onChange={(event) => {

setQuery(event.target.value);

}}

placeholder="Поиск документов"

/>

);

}

Но отправлять API-запрос при каждом изменении значения без дополнительных ограничений не стоит.

49. Что такое Debounce

Debounce Search React — задержка выполнения операции до тех пор, пока пользователь не перестанет вводить текст на определённое время.

Например:

User types:

p

po

pos

post

Wait 300 ms

Send Search Request

Это уменьшает количество запросов.

50. Как реализовать useDebounce

React useDebounce Hook:

import { useEffect, useState } from "react";

export function useDebounce<T>(

value: T,

delay: number

): T {

const [debouncedValue, setDebouncedValue] =

useState(value);

useEffect(() => {

const timer = setTimeout(() => {

setDebouncedValue(value);

}, delay);

return () => {

clearTimeout(timer);

};

}, [value, delay]);

return debouncedValue;

}

Использование:

const debouncedQuery = useDebounce(query, 300);

Теперь запрос можно запускать после небольшой паузы.


51. Как использовать TanStack Query

TanStack Query Search React:

import { useQuery } from "@tanstack/react-query";

function useDocumentSearch(

organizationId: string,

query: string

) {

return useQuery({

queryKey: [

"search",

organizationId,

query

],

queryFn: async ({ signal }) => {

const params = new URLSearchParams({

q: query

});

const response = await fetch(

`/api/organizations/{params}`,

{ signal }

);

if (!response.ok) {

throw new Error("Search failed");

}

return response.json();

},

enabled:

Boolean(organizationId) &&

query.trim().length >= 2,

});

}

Tenant ID обязательно включается в Query Key.

Это помогает разделять кеш между организациями.

Но Backend всё равно должен проверять права.

52. Как избежать Race Conditions

Search Request Race Condition:

Request A: "post"

Request B: "postgres"

Request B finishes first

Request A finishes later

Если интерфейс не контролирует актуальность запросов, старые результаты могут заменить новые.

TanStack Query помогает управлять состоянием запросов по ключам.

Дополнительно можно использовать AbortSignal для отмены устаревших запросов.

53. Как отображать Search States

Search UI States:

Idle

Loading

Success

Empty

Error

Пользователь должен понимать, что происходит.

Например:

  • «Начните вводить запрос».

  • «Поиск…».

  • «Найдено 24 документа».

  • «Ничего не найдено».

  • «Не удалось выполнить поиск».

Это улучшает UX и снижает количество повторных запросов.


Синхронизация PostgreSQL и Elasticsearch

54. Почему поисковый индекс может устаревать

Search Index Synchronization — одна из главных сложностей отдельного поискового движка.

Например:

User updates document

|

v

PostgreSQL updated

|

v

Elasticsearch not updated

Теперь пользователь может увидеть устаревший результат.

Поэтому необходимо определить механизм синхронизации.

55. Почему Dual Write опасен

Dual Write Problem возникает, когда приложение записывает данные в две системы.

Например:

Write PostgreSQL

|

v

Write Elasticsearch

Если первая операция успешна, а вторая завершается ошибкой, системы расходятся.

Обратный порядок тоже не решает проблему полностью.

Нужен механизм повторной обработки и восстановления согласованности.

56. Как использовать Transactional Outbox

Transactional Outbox Search Indexing:

FastAPI

|

v

PostgreSQL Transaction

|

+-- UPDATE document

|

+-- INSERT outbox_event

|

v

COMMIT

|

v

Outbox Worker

|

v

Elasticsearch / OpenSearch

Так бизнес-изменение и событие для индексации сохраняются в одной транзакции.

Worker может повторить индексацию после временной ошибки.

57. Как обеспечить Idempotent Indexing

Idempotent Search Indexing позволяет безопасно повторять обработку событий.

Например, Document ID используется как Elasticsearch Document ID.

Повторная индексация обновляет ту же запись, а не создаёт новую.

Но необходимо учитывать порядок событий.

Если старое событие обработается после нового, оно может перезаписать актуальные данные.

Для защиты можно использовать версии документов или получать актуальное состояние из PostgreSQL.

58. Как обрабатывать удаление документов

Search Index Delete Synchronization:

Document deleted in PostgreSQL

|

v

Outbox Event

|

v

Search Worker

|

v

Delete from Search Index

Удаление особенно важно для безопасности.

Если документ удалён или доступ к нему отозван, устаревший индекс не должен раскрывать его содержимое.

Для чувствительных данных полезно повторно проверять права и существование документа перед выдачей результата.


59. Что такое Reindexing

Elasticsearch Reindexing Strategy — процесс перестроения поискового индекса.

Он может понадобиться при:

  • изменении Mapping;

  • изменении Analyzer;

  • миграции схемы;

  • исправлении повреждённых данных;

  • обновлении логики индексации.

Для больших индексов Reindexing может быть дорогой операцией.

60. Как использовать Index Aliases

Elasticsearch Index Alias позволяет переключать приложение между индексами.

Например:

documents_v1

documents_v2

Приложение обращается к Alias:

documents_current

После завершения Reindexing Alias можно переключить на новый индекс.

Это помогает уменьшить время недоступности поиска.

Но необходимо отдельно продумать обработку изменений данных, произошедших во время перестроения.


Производительность и масштабирование

61. Как измерять производительность поиска

Search Performance Metrics:

  • Search Latency;

  • P50;

  • P95;

  • P99;

  • Query Throughput;

  • Indexing Latency;

  • Error Rate;

  • Cache Hit Rate;

  • Search Zero Results Rate.

Например:

P50: 40 ms

P95: 120 ms

P99: 350 ms

Это иллюстративные значения.

Реальные показатели зависят от объёма данных, запросов, инфраструктуры и нагрузки.

62. Как анализировать PostgreSQL Search

EXPLAIN ANALYZE Full Text Search:

EXPLAIN (ANALYZE, BUFFERS)

SELECT id, title

FROM documents

WHERE search_vector @@

plainto_tsquery(

'english',

'database'

);

План выполнения помогает понять:

  • используется ли индекс;

  • сколько строк обрабатывается;

  • сколько времени занимает запрос;

  • сколько страниц читается.

Не стоит создавать индексы только на основании предположений.

63. Как масштабировать Elasticsearch

Elasticsearch Horizontal Scaling включает:

  • Shards;

  • Replicas;

  • Data Nodes;

  • Search Coordination;

  • управление памятью;

  • мониторинг диска;

  • настройку Refresh;

  • оптимизацию Mapping.

Но увеличение количества Shards не всегда ускоряет поиск.

Слишком много мелких Shards может увеличивать накладные расходы.

64. Как работает Search Caching

Search Results Caching Redis может ускорять повторяющиеся запросы.

Например:

Search Query

|

v

Redis Cache

|

+-- Cache Hit

|

+-- Cache Miss

|

v

Search Engine

Но в Multi-Tenant SaaS Cache Key должен учитывать:

  • Tenant;

  • Query;

  • Filters;

  • Sort;

  • Permissions или их безопасный контекст.

Нельзя использовать общий кеш, который может вернуть результаты другого пользователя.


Безопасность поиска

65. Как защитить Search API

Search API Security Checklist:

  • Authentication;

  • Authorization;

  • Tenant Isolation;

  • Rate Limiting;

  • Query Length Limits;

  • Safe Query Parsing;

  • SQL Parameterization;

  • ограничение Pagination;

  • контроль нагрузки;

  • безопасное логирование.

Поисковые Endpoint могут быть дорогими.

Поэтому необходимо защищать их от чрезмерного количества запросов.

66. Почему Search Highlighting может быть опасен

Search Highlighting XSS — риск при отображении фрагментов текста с выделением совпадений.

Например, поисковый движок возвращает HTML-разметку:

<em>database</em>

Если Frontend вставляет такой HTML без безопасной обработки, недоверенный текст может привести к XSS.

Лучше использовать безопасный рендеринг или строго контролируемую Sanitization.

67. Как защитить Elasticsearch от прямого доступа

Elasticsearch Production Security требует:

  • не публиковать кластер напрямую в интернет без необходимости;

  • использовать Authentication;

  • настраивать TLS;

  • ограничивать сетевой доступ;

  • применять минимальные права;

  • защищать Credentials;

  • обновлять версии;

  • мониторить подозрительные запросы.

React не должен напрямую обращаться к Elasticsearch с привилегированными Credentials.

Обычно поисковые запросы проходят через Backend.


68. Как выбрать PostgreSQL, Elasticsearch или OpenSearch

PostgreSQL vs Elasticsearch vs OpenSearch:

Критерий

PostgreSQL

Elasticsearch

OpenSearch

Простота запуска

Высокая при существующей БД

Ниже

Ниже

Full-Text Search

Да

Да

Да

Fuzzy Search

Через расширения и дополнительные запросы

Да

Да

Распределённый поиск

Ограниченные встроенные возможности

Да

Да

Сложные Aggregations

SQL

Специализированные

Специализированные

Отдельная синхронизация

Не нужна

Нужна

Нужна

Операционная сложность

Обычно ниже

Выше

Выше

Лучший сценарий

Поиск внутри приложения

Сложный специализированный поиск

Сложный специализированный поиск

Для небольшого SaaS PostgreSQL Full-Text Search часто является хорошей отправной точкой.

Если требования к поиску становятся сложнее, можно добавить отдельный поисковый движок.

69. Как мигрировать с PostgreSQL Search на Elasticsearch

PostgreSQL to Elasticsearch Migration:

  1. Определить Search API Contract.

  2. Выделить Search Service Interface.

  3. Создать Elasticsearch Mapping.

  4. Настроить первоначальную индексацию.

  5. Добавить Transactional Outbox.

  6. Синхронизировать изменения.

  7. Сравнить результаты поиска.

  8. Провести Load Testing.

  9. Переключить часть трафика.

  10. Контролировать ошибки и релевантность.

Такой подход позволяет менять поисковый движок без полного переписывания Frontend.


70. Рекомендуемая архитектура поиска для SaaS

Full Stack Search Architecture React FastAPI PostgreSQL:

React Frontend

|

v

Search UI

|

v

FastAPI API

|

Authentication

|

v

Tenant Context

|

v

Search Service

|

+------------+------------+

| |

v v

PostgreSQL Elasticsearch

| |

v v

Source of Truth Search Index

|

v

Outbox Events

|

v

Indexing Worker

|

v

Elasticsearch

В этой архитектуре PostgreSQL остаётся основным источником данных.

Elasticsearch отвечает за специализированный поиск.

FastAPI проверяет права и формирует поисковые запросы.

React отображает результаты.

Outbox Worker синхронизирует индекс.

Но если PostgreSQL Search удовлетворяет требованиям, отдельный Elasticsearch можно не использовать.


71. Типичные ошибки при реализации поиска

Первая ошибка — использовать ILIKE '%query%' для любого объёма данных без анализа производительности.

Вторая — внедрять Elasticsearch до появления реальной необходимости.

Третья — забывать Tenant Filter.

Четвёртая — доверять Organization ID от клиента.

Пятая — строить SQL через конкатенацию строк.

Шестая — не ограничивать длину поискового запроса.

Седьмая — не использовать Debounce в Autocomplete.

Восьмая — отправлять запрос после каждого нажатия клавиши без ограничений.

Девятая — не учитывать Race Conditions.

Десятая — не включать Tenant ID в Query Cache Key.

Одиннадцатая — использовать Text вместо Keyword для точных идентификаторов.

Двенадцатая — неправильно выбирать Analyzer.

Тринадцатая — не тестировать релевантность.

Четырнадцатая — не учитывать Near Real-Time индексацию.

Пятнадцатая — использовать ненадёжный Dual Write.

Шестнадцатая — не обрабатывать удаление документов.

Семнадцатая — не реализовывать Reindexing.

Восемнадцатая — создавать слишком много Shards.

Девятнадцатая — открывать Elasticsearch напрямую в интернет.

Двадцатая — не контролировать Search Latency.

Эти ошибки Search Architecture могут приводить к медленному поиску, устаревшим результатам, утечкам данных и высокой стоимости инфраструктуры.


72. Чек-лист поиска для Production

Search Engine Production Checklist:

PostgreSQL

  • Выбрана подходящая текстовая конфигурация.

  • Используется GIN Index.

  • Проверены планы выполнения.

  • Настроено ранжирование.

  • Ограничен размер запроса.

  • Используются параметризованные SQL-запросы.

  • Учтён Tenant Context.

Elasticsearch / OpenSearch

  • Mapping определён явно.

  • Правильно выбраны Text и Keyword.

  • Настроены Analyzers.

  • Определена стратегия Sharding.

  • Настроены Replicas при необходимости.

  • Реализована синхронизация индекса.

  • Обрабатываются Delete Events.

  • Есть Reindexing Strategy.

  • Настроен мониторинг кластера.

  • Ограничен сетевой доступ.

Frontend

  • Есть Debounce.

  • Обрабатываются Loading и Error States.

  • Используется безопасное отображение Highlighting.

  • Реализована Pagination.

  • Кеш разделён по Tenant.

  • Устаревшие запросы не заменяют актуальные результаты.

  • Autocomplete ограничен по нагрузке.

Security

  • Search API требует Authentication.

  • Проверяется Authorization.

  • Реализована Tenant Isolation.

  • Настроен Rate Limiting.

  • Нет прямого доступа к Search Credentials.

  • Facets не раскрывают чужие данные.

  • Удалённые документы не доступны через устаревший индекс.

Monitoring

  • Измеряется P95 Search Latency.

  • Мониторятся Search Errors.

  • Контролируется Indexing Lag.

  • Есть Alerts.

  • Выполняется Load Testing.

  • Проверяется качество релевантности.


FAQ

Что такое Full-Text Search?

Механизм поиска по текстовым документам с учётом токенизации, лексем и других правил обработки текста.

Можно ли реализовать поиск только на PostgreSQL?

Да. Для многих приложений PostgreSQL Full-Text Search достаточно.

Что такое tsvector?

Тип данных PostgreSQL для представления подготовленного текста.

Что такое tsquery?

Тип данных PostgreSQL для представления поискового запроса.

Зачем нужен GIN Index?

Для ускорения поиска по индексируемым значениям, включая Full-Text Search.

Что такое ts_rank?

Функция PostgreSQL для оценки релевантности документов.

Поддерживает ли PostgreSQL русский язык?

Да, PostgreSQL предоставляет конфигурацию Full-Text Search для русского языка.

Как реализовать поиск с опечатками?

В PostgreSQL можно использовать pg_trgm, а в Elasticsearch — Fuzzy Matching.

Что лучше: PostgreSQL или Elasticsearch?

PostgreSQL обычно проще для поиска внутри небольшого приложения. Elasticsearch полезен при сложных поисковых требованиях.

Что такое OpenSearch?

Отдельная поисковая и аналитическая платформа, основанная на Apache Lucene.

Elasticsearch и OpenSearch полностью совместимы?

Нет. Совместимость зависит от версий, API и используемых возможностей.

Что такое Analyzer?

Компонент поискового движка, преобразующий текст в поисковые токены.

Что такое BM25?

Алгоритм ранжирования документов по релевантности текстовому запросу.

Что такое Autocomplete?

Механизм подсказок во время ввода поискового запроса.

Что такое Debounce?

Задержка запуска операции до завершения активного ввода.

Что такое Faceted Search?

Поиск с агрегированными фильтрами по категориям и другим полям.

Что такое Near Real-Time Search?

Модель, при которой изменения становятся видимыми поиску с небольшой задержкой.

Как синхронизировать PostgreSQL и Elasticsearch?

Через события, очереди и надёжные механизмы вроде Transactional Outbox.

Что такое Reindexing?

Перестроение поискового индекса, например после изменения Mapping или Analyzer.

Как защитить поиск в Multi-Tenant SaaS?

Использовать доверенный Tenant Context, фильтры доступа и дополнительные проверки Authorization.

Какой поисковый стек выбрать для React + FastAPI?

На старте часто достаточно PostgreSQL Full-Text Search. Elasticsearch или OpenSearch можно добавить по мере роста требований.


Заключение

Поиск — одна из ключевых функций современных веб-приложений.

Но качественный поиск требует не только SQL-запроса, а продуманной архитектуры.

PostgreSQL Full-Text Search позволяет создавать полнотекстовый поиск без отдельного поискового сервера.

GIN Index ускоряет поиск по подготовленным текстовым данным.

ts_rank помогает ранжировать результаты.

pg_trgm позволяет реализовать поиск с опечатками.

Elasticsearch и OpenSearch предоставляют дополнительные возможности для сложного поиска, агрегатов, анализаторов и распределённой инфраструктуры.

Но отдельный поисковый движок требует синхронизации с основной базой данных.

Для этого полезен Transactional Outbox.

React обеспечивает удобный интерфейс поиска с Debounce, Autocomplete и управлением состоянием запросов.

FastAPI отвечает за Authentication, Authorization и Tenant Isolation.

При этом безопасность поиска должна охватывать не только основные результаты, но и подсказки, агрегаты, кеш и устаревшие индексированные документы.

Главный принцип: начинайте с самого простого поискового решения, которое удовлетворяет требованиям продукта, и усложняйте архитектуру только при появлении измеримых ограничений.

Для большинства небольших Full Stack проектов разумно сначала попробовать PostgreSQL Full-Text Search.

Когда появятся сложные требования к релевантности, большим объёмам документов или специализированной аналитике, можно рассмотреть Elasticsearch или OpenSearch.

Так вы получите производительный поиск без преждевременного усложнения инфраструктуры.


Наверх