meta(search): оверхол поиска — гибрид лексика+вектор, единый путь (ru_en, OR+операторы, RRF, TEI e5-small) #527
Open
opened 2026-07-12 01:15:06 +03:00 by agent_vscode
·
1 comment
No Branch/Tag Specified
main
develop
feat/530-search-semantic
feat/568-page-history-redesign
feat/561-comments-redesign
feat/566-worktime-modal-redesign
feat/370-agent-save-version
recover/506-apikey-ui
feat/370-share-approved
fix/520-recovery-escalation
fix/554-literal-br
feat/529-search-lexical
feat/501-mcp-apikey-auth
feat/506-apikeys-ui
feat/395-work-time
feat/502-mcp-md-modes
feat/492-incremental-render
fix/549-hardbreak-md-canon
feat/542-resolve-undo
fix/505-drawio-strip-comments
fix/503-hardbreak-canon
docs/471-breadcrumb-permission
fix/534-mcp-spaceid
feat/481-version-coherence
fix/535-list-seam-coalesce
fix/525-insert-unloaded
fix/522-internal-links
fix/507-drawio-cyrillic
fix/541-reconnect-timeout
feat/493-converter
feat/370-page-versioning
fix/515-code-emphasis
fix/523-tree-dnd
fix/517-toasts
feat/495-tails
feat/494-mcp-registry-guards
feat/496-suggestions-audit
integ/497-wave1
feat/488-client-fsm
ci/476-publish-gates
feat/489-tool-resilience
feat/487-terminal-lifecycle
fix/486-iter1-prod-fires
refactor/347-client-md-paste
perf/479-getpage-cache
refactor/449-write-invariants
refactor/450-split-client
feat/443-get-page-context
feat/443-get-tree
feat/443-search
docs/415-lossy-descriptions
feat/413-markdown-default
refactor/412-camelcase-tool-names
feat/425-drawio-graph
fix/464-diffdocs-cpu-guard
refactor/411-update-page-markdown
feat/424-drawio-rules
fix/409-invalid-node-validation
refactor/448-generate-inventory
refactor/445-execute-mapping
refactor/446-derive-client-types
fix/447-registry-stamp-ci
fix/444-agent-loop-guards
fix/396-sendnow-detached-run
fix/452-immutable-cache-control
docs/footnote-authoring-comment-cleanup
feat/437-error-diagnostics
fix/442-cursor-pagination
feat/419-footnote-normalize
perf/399-comment-resolve-async
perf/435-collab-token-cache
perf/344-background-rerenders
perf/348-backend-lowhanging
feat/423-drawio-crud
perf/401-collab-hotpath
feat/430-live-reconnect
perf/400-collab-session
refactor/414-dedup-node-ops
feat/417-new-comments-signal
feat/408-createcomment-hints
feat/407-persist-tool-errors
feat/420-footnote-render
dummy-review/mcp-hang-fix
dummy-review/mcp-hang-base
feat/381-resumable-sse-pr1
feat/git-sync-2
docs/agents-workspace-build-order
feature/offline-sync
perf/342-code-splitting
perf/346-compression-cache
feat/196-multi-cursor
perf/343-typing-latency
docs/how-to-test
fix/ai-sdk-partial-output-oom
test/351-generative-converter
feat/371-roles-catalog
refactor/345-server-converter
refactor/294-spec-registry-cont
fix/363-migration-order
fix/e2e-callout-and-gate-build
fix/docker-re2-toolchain
feat/git-sync
fix/media-roundtrip-stability
fix/340-comment-panel-perf
fix/332-deferred-tools
fix/329-ephemeral-suggestions
fix/330-search-in-page
fix/328-resolved-anchor-spam
fix/331-intraline-diff
fix/324-coverage-gate
fix/325-mobile-390
feat/293-A-git-sync-package
feat/300-avatar-oklch
fix/321-banner-mobile
feat/300-avatar-colors
feat/315-comment-suggestions
feat/scroll-restore-stable-wait
feat/300-agent-avatar-stack
feat/300-avatar-polish
refactor/294-tool-spec-registry
feat/scroll-restore-ux
fix/responsive-tablet-sidebar
feature/ai-chat-page-change-observability
image-inline-center
fix/283-short-remap-title
fix/283-slash-layout
image-inline-row
feat/276-ai-chat-dock
fix/269-table-menu-refocus
docs/dev-stand-guide
feat/266-scroll-position
fix/260-collab-docname-slugid
test/244-phase2-tail
fix/262-reindex-progress-realtime
fix/258-changelog-compare-links
fix/244-dataloss-bugs
feat/246-spoiler
feat/221-image-captions
test/244-part-b
feat/251-intentional-clear
fix/embeddings-reindex-progress
refactor/193-tool-spec-registry
fix/255-ws-redis-adapter-leak
fix/252-e2e-open-handles
feat/229-catalog-yaml
feat/243-blob-sandbox
feat/228-inline-footnotes
fix/qa-ui-bugs-216-218
feature/agent-roles-catalog
fix/share-alias-rename
fix/ai-chat-empty-render
feat/191-chat-doc-binding
feat/201-temporary-notes
feat/198-interrupt-agent
feat/ai-chat-full-history
feat/199-ai-generate-title
feat/205-share-aliases
batch/issues-189-187-170
feat/170-mcp-test-button
feat/189-context-badge
feat/198-interrupt-agent-send-now
fix/issues-190-159
fix/ai-chat-new-chat-during-stream
fix/ai-chat-stream-perf
batch/issues-2026-06-25
feat/ai-chat-persistent-history
fix/ai-chat-copy-chat-wysiwyg
fix/ai-stream-reset-resilience
fix/ai-stream-undici-timeout
fix/footnote-review-1227-followup
fix/ai-chat-token-counter-realtime
docs/manual-qa-test-plan
v0.94.1
v0.94.0
v0.93.0
Labels
Clear labels
epic
needs-human
review/approved
review/changes-requested
review/needs
возможно, баг
Large multi-phase effort spanning many changes
эскалация: нужно решение человека
в последнем ревью нет открытых blocking-находок
последнее ревью оставило открытые blocking-находки
head не ревьюился (head != reviewed_head)
Похоже на баг, но требует подтверждения/воспроизведения; чиним, если повторится
No Label
Milestone
No items
No Milestone
Projects
Clear projects
No project
No Assignees
Notifications
Due Date
No due date set.
Dependencies
No dependencies set.
Reference: vvzvlad/gitmost#527
Reference in New Issue
Block a user
Blocking a user prevents them from interacting with repositories, such as opening or commenting on pull requests or issues. Learn more about blocking a user.
Delete Branch "%!s()"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
Проблема / Цель
Пользовательский и агентский поиск дают ложные пустые выдачи на нормальных запросах и не ранжируют по релевантности. Замерено на живой базе (спейс Obsidian, через MCP
search):[](строгий AND: слова на разных страницах).[]при существующей странице «ресторан в москве» (english-конфиг FTS не стеммит русский: «ресторанов»≠«ресторан», «москвы»≠«москве»).Цель: лучший поиск — предсказуемое релевантностное ранжирование (OR-дефолт), русская+английская морфология, операторы запроса, пагинация, гибрид лексика+вектор (семантика), единый движок для веб-UI, MCP-агента и share. Пустая выдача — только когда реально ничего не совпало.
Контекст и исследование
Затронутые подсистемы (репозиторий,
SEARCH_DRIVER=database; наtypesenseповедение не меняется — вне scope):apps/server/src/core/search/search.service.ts— два пути вsearchPage(): FTS (веб-UI,to_tsquery('english', …), строгий AND) и lookup (substring:true, всегда шлёт MCP; гибрид LIKE-всей-строки + FTS, тир-ранжирование, path/snippet, без total/hasMore/offset, cap 200).tsv— колонка через триггерpages_tsvector_trigger(миграции20240324T086800,20250729T213756):setweight(to_tsvector('english', f_unaccent(title)),'A') || setweight(to_tsvector('english', f_unaccent(text_content)),'B').page_embeddings(pgvector, chunk-level) +hybridSearch()RRF, k=60 (apps/server/src/database/repos/ai-chat/page-embedding.repo.ts) — используется только RAG AI-агента, не пользовательским поиском. Вектор dimension-agnostic → seq scan (нет ANN).page_embeddings.fts— generatedto_tsvector('english', …); query-конфиг вhybridSearchзахардкоженwebsearch_to_tsquery('english', …)(:252).embedding-indexer.service.tsпо событиямPAGE_CREATED/UPDATED; провайдер per-workspace (ai_provider_credentials); сейчас документы эмбедятся без e5-префикса; no-op без провайдера.filterAccessiblePageIds(page-permission.repo.ts) — fast-path #348: ранний выход без рекурсивного CTE, когда ограниченных страниц в скоупе нет.SEARCH_DRIVER=database|typesense(environment.*): typesense-контроллер обходитsearchPage().search()всегда шлётsubstring:true;tool-specs.ts+server-instructions.ts(enforced тестом).Смежное: #443 (closed) осознанно развёл веб-поиск (in-app RRF) и агентский (substring) — этот оверхол разворот объединяет их (по цели «лучший поиск в UI, лексика+вектора»). #471 (open) — утечка title предка в breadcrumbs (см. ниже, переоценено). #293 — дрейф зеркал схемы (наш парсер не должен создавать новое зеркало: единый источник — сервер).
Дизайн (обзор; детали — в ишью фаз)
Единый
SearchServiceдля UI/агента/share. Внутри — ветки, слитые единым примитивом RRF (k=60): (1) лексикаpages.tsv(ru_en,ts_rank_cd, веса A/B), (2) substring/trigram для ID/host (match=auto), (3) [фаза B] векторpage_embeddings(cosine, chunk→page). Три несопоставимые шкалы (ts_rank/тир/cosine) НЕ складываются — каждая ветка ранжируется отдельно, RRF комбинирует ранги.Единый bilingual конфиг
ru_en(russian_stem для кириллицы + english_stem для латиницы) во всех местах: триггерpages.tsv, generatedpage_embeddings.fts, и query-конфигhybridSearch(:252) — конфиг колонки и конфиг запроса всегда меняются парой (иначе тихо ломается лексическое плечо RAG).Жизненный цикл семантики (спроектирован целиком, реализуется в B)
Ключ версии эмбеддингов = фингерпринт
{modelId + revision + prefixScheme + dimensions}(не голоеmodelName— иначе смена revision/префикса не детектится). Активный фингерпринт хранится в настройках воркспейса; вектор-CTE и подсчёт покрытия фильтруют по нему.semantic.available=false, reason=no-providerreason=staleРеиндекс при смене фингерпринта: новые строки пишутся с новым фингерпринтом, старые не удаляются до конца; указатель активного фингерпринта флипается атомарно только после полного реиндекса (и только если конфиг всё ещё == target); затем идемпотентный GC (кап 2 генерации, ~2× storage на время).
deleteByPageво время свапа — model-scoped (иначе правка страницы выкидывает её из семантики). Знаменатель покрытия = страницы, реально давшие ≥1 chunk.Сквозные инварианты
hasRestricted, чтобы сохранить fast-path #348); ошибка permission-запроса → 500, НЕ пустая лексика.total— permission-filtered (не течёт кардинальностью скрытых).semanticв ответе (серверной метрик-инфраструктуры в проекте нет — не обещаем метрик-сток). try/catch деградации оборачивает только embed/вектор-ветку.CANDIDATE_CAP(env, 500) — окно фьюжна; детерминированныйORDER BY rrf DESC, id;hasMore; при упоре в CAP —truncatedAtCap:true(хвост за CAP недостижим — зафиксировать в tool-spec).Границы scope
SEARCH_DRIVER=database. Typesense не трогаем.Фазы (крупная нарезка)
page_embeddings.ftsиhybridSearch:252), парсер с операторами, OR+релевантность через RRF (лексика+substring), match=auto, пагинация/total, единый путь UI/агент/share, superset ответа, MCP+web-UI. Independently shippable — даёт лучший поиск без вектора.hybridSearchPages(добавляет вектор-ветку в RRF), фингерпринт+lifecycle семантики, best-effort gating, наблюдаемость.Совместимость и миграции
SEARCH_MODE=or|and— тумблер парсера (оба валидны на ru_en-tsv), НЕ откат конфига. Настоящий откат = down-миграция + полный реиндекс (минуты-часы, задокументировано).migration_complete) или dual-config на окно (искать по обоим конфигам), чтобы страницы, правленые в окне, не давали морфологический рассинхрон; реиндекс — процедурный батч-джоб вне единой Kysely-транзакции (SEARCH_REINDEX_BATCH, tsv через touch строк);page_embeddings.fts— бэкфиллом без длинного ACCESS EXCLUSIVE. down(): сперва вернуть tsv/fts на english, потом DROP объекта конфига ru_en (иначе падение по зависимостям).Связи
Разворачивает #443. Ссылается на #471 (path-утечка — переоценено в ишью A), #293 (не плодить зеркала — парсер на сервере, единый источник). Фазы: A → B (B blocked by A).
Фазы поданы:
Порядок: #529 → #530.