Skip to content

ENG-1734 Add Roam semantic search to Advanced Node Search#1186

Open
trangdoan982 wants to merge 11 commits into
mainfrom
eng-1734-add-semantic-search-for-sync-enabled-graphs
Open

ENG-1734 Add Roam semantic search to Advanced Node Search#1186
trangdoan982 wants to merge 11 commits into
mainfrom
eng-1734-add-semantic-search-for-sync-enabled-graphs

Conversation

@trangdoan982

@trangdoan982 trangdoan982 commented Jul 3, 2026

Copy link
Copy Markdown
Member

https://www.loom.com/share/acfb9a8fcae24dea892a655974210c52

Summary

  • Add a reusable discourseNodeSemanticSearch orchestrator that runs Roam semantic search when semanticSearchEnabled() is true, filters to discourse notes, and falls back to MiniSearch when fewer than five post-filter results remain or the API errors
  • Wire Advanced Node Search (dialog + sidebar) to the orchestrator with async search, cancellation, and relevance sorting that keeps semantic hits before keyword hits
  • Add unit tests covering the gate, threshold, combine/dedupe, and error fallback paths

Test plan

  • pnpm --filter roam test
  • pnpm --filter roam check-types
  • pnpm --filter roam lint
  • Reload Roam dev extension and verify Advanced Node Search with semantic search enabled returns discourse notes for meaning-based queries
  • Verify sparse semantic results (< 5) append keyword matches after semantic hits
    ->> I wasn't able to test this because semantic search always returns more than enough results
  • Verify semantic search disabled uses MiniSearch-only behavior (unchanged)
  • Cross-check queries in admin Search Test tab (roamSemantic vs miniSearch)

Made with Cursor


Open in Devin Review

Summary by CodeRabbit

  • New Features

    • Advanced node search now combines semantic and keyword-based results for better coverage.
    • Searches can now return more relevant matches even when one search method finds only a few results.
  • Bug Fixes

    • Search results are now cleared correctly for empty, matching, or unavailable queries.
    • Improved search stability to avoid outdated results appearing after rapid query changes.
  • Refactor

    • Search ranking and result handling were updated to support richer result data and more consistent sorting.

Wire Advanced Node Search to Roam semantic search when enabled, with discourse-note filtering and MiniSearch fallback when fewer than five semantic results remain.

Co-authored-by: Cursor <cursoragent@cursor.com>
@vercel

vercel Bot commented Jul 3, 2026

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

1 Skipped Deployment
Project Deployment Actions Updated (UTC)
discourse-graph Skipped Skipped Jul 4, 2026 2:37am

Request Review

@linear-code

linear-code Bot commented Jul 3, 2026

Copy link
Copy Markdown

ENG-1734

@supabase

supabase Bot commented Jul 3, 2026

Copy link
Copy Markdown

This pull request has been ignored for the connected project zytfjzqyijgagqxrzbmz because there are no changes detected in packages/database/supabase directory. You can change this behaviour in Project Integrations Settings ↗︎.


Preview Branches by Supabase.
Learn more about Supabase Branching ↗︎.

Co-authored-by: Cursor <cursoragent@cursor.com>

@devin-ai-integration devin-ai-integration Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Devin Review found 3 potential issues.

Open in Devin Review

Comment thread apps/roam/src/components/AdvancedNodeSearchDialog/useAdvancedNodeSearchResults.ts Outdated
Comment thread apps/roam/src/components/AdvancedNodeSearchDialog/useAdvancedNodeSearchResults.ts Outdated
Return frozen docked results synchronously and avoid persisting empty results that clobber the sidebar registry on mount.

Co-authored-by: Cursor <cursoragent@cursor.com>
Use main-branch inline docked query check, remove dockedSearchSnapshot helper and Roam tests, and apply sort in useMemo so sort changes do not re-run async search.

Co-authored-by: Cursor <cursoragent@cursor.com>
Co-authored-by: Cursor <cursoragent@cursor.com>
Extract shared search hit types and remove DiscourseSearchHit adapter conversions in the Advanced Node Search hook.

Co-authored-by: Cursor <cursoragent@cursor.com>
Use searchDiscourseNodes for the semantic-plus-miniSearch orchestrator, searchDiscourseNodesWithMiniSearch for MiniSearch, and ScoredSearchResult instead of hit-based names.

Co-authored-by: Cursor <cursoragent@cursor.com>
Sort runs in useMemo only; the search effect does not depend on sort config.

Co-authored-by: Cursor <cursoragent@cursor.com>
Comment thread apps/roam/src/components/AdvancedNodeSearchDialog/utils.ts
Co-authored-by: Cursor <cursoragent@cursor.com>
trangdoan982 and others added 2 commits July 3, 2026 22:36
Drop temporary search logging, clear stale results when docking or starting a new search, and keep semantic hits first when sorting by relevance.

Co-authored-by: Cursor <cursoragent@cursor.com>
@trangdoan982

Copy link
Copy Markdown
Member Author

Addressed open review feedback in 7acc534:

  • Docked query stale results: clear unsortedScoredResults when isDockedQuery and before starting a new async search.
  • Relevance sort: semantic hits stay first regardless of sort direction; score direction still applies within each source group.
  • Debug logs: removed temporary logDiscourseNodeSearchResults / DEBUG_DISCOURSE_NODE_SEARCH logging added for manual testing.

Also merged latest main. CI is green.

@trangdoan982 trangdoan982 requested a review from mdroidian July 4, 2026 03:27
@mdroidian

Copy link
Copy Markdown
Member

@coderabbitai full review

@coderabbitai

coderabbitai Bot commented Jul 5, 2026

Copy link
Copy Markdown
Contributor
✅ Action performed

Full review finished.

@coderabbitai

coderabbitai Bot commented Jul 5, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

📝 Walkthrough

Walkthrough

Refactors discourse node search into a shared type system (SearchResult, ScoredSearchResult, DiscourseNodeSearchSource) with merge/conversion helpers, adds a new searchDiscourseNodes function combining semantic and miniSearch results, exports runRoamSemanticSearch, and converts the advanced search hook to an async, effect-driven pipeline.

Changes

Semantic + MiniSearch Discourse Node Search

Layer / File(s) Summary
Shared search result types and merge helpers
apps/roam/src/utils/discourseNodeSearchTypes.ts
Adds SearchResult, ScoredSearchResult, DiscourseNodeSearchSource types, plus combineSemanticAndMiniSearchResults (dedupe by uid) and toScoredSearchResultFromSemantic (builds result from semantic input or resultsByUid lookup).
searchDiscourseNodes orchestration and semantic provider export
apps/roam/src/utils/searchDiscourseNodes.ts, apps/roam/src/utils/discourseNodeSearchProviders.ts
Adds isRoamSemanticSearchEnabled and searchDiscourseNodes, which falls back to miniSearch when semantic search is disabled/errored/insufficient, otherwise returns or merges semantic results; exports runRoamSemanticSearch from providers module.
AdvancedNodeSearchDialog utils type and sort updates
apps/roam/src/components/AdvancedNodeSearchDialog/utils.ts
Replaces local SearchResult/ScoredSearchHit types with shared imports, updates sortSearchResults to accept scoredResults with source-aware relevance ordering, and updates searchIndexedNodes to return ScoredSearchResult tagged with source: "miniSearch".
Async search hook rewrite
apps/roam/src/components/AdvancedNodeSearchDialog/useAdvancedNodeSearchResults.ts
Converts the synchronous useMemo search to a stateful async effect calling searchDiscourseNodes with a runMiniSearch fallback, guarded by a cancellation flag, then sorts results via sortSearchResults before returning docked or sorted results.

Estimated code review effort: 3 (Moderate) | ~30 minutes

Sequence Diagram(s)

sequenceDiagram
  participant Hook as useAdvancedNodeSearchResults
  participant Search as searchDiscourseNodes
  participant Semantic as runRoamSemanticSearch
  participant Mini as runMiniSearch

  Hook->>Search: query, nodeTypes, resultsByUid, runMiniSearch
  Search->>Search: check semantic search enabled
  alt semantic disabled or errors
    Search->>Mini: runMiniSearch()
    Mini-->>Search: ScoredSearchResult[]
  else semantic enabled
    Search->>Semantic: query
    Semantic-->>Search: filteredResults
    alt sufficient semantic results
      Search-->>Hook: semantic ScoredSearchResult[]
    else insufficient results
      Search->>Mini: runMiniSearch()
      Mini-->>Search: ScoredSearchResult[]
      Search->>Search: combineSemanticAndMiniSearchResults
    end
  end
  Search-->>Hook: ScoredSearchResult[]
  Hook->>Hook: sortSearchResults(scoredResults, sort)
Loading
🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly and concisely describes the main change: adding Roam semantic search to Advanced Node Search.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.

Warning

There were issues while running some tools. Please review the errors and either fix the tool's configuration or disable the tool if it's a critical failure.

🔧 ESLint

If the error stems from missing dependencies, add them to the package.json file. For unrecoverable errors (e.g., due to private dependencies), disable the tool in the CodeRabbit configuration.

apps/roam/src/components/AdvancedNodeSearchDialog/useAdvancedNodeSearchResults.ts

Oops! Something went wrong! :(

ESLint: 8.57.1

YAMLException: Cannot read config file: /apps/roam/eslint.config.mjs
Error: end of the stream or a document separator is expected (8:20)

5 | export default [
6 | ...config,
7 | {
8 | languageOptions: {
------------------------^
9 | parserOptions: {
10 | tsconfigRootDir: ".",
at generateError (/node_modules/.pnpm/js-yaml@4.1.0/node_modules/js-yaml/lib/loader.js:183:10)
at throwError (/node_modules/.pnpm/js-yaml@4.1.0/node_modules/js-yaml/lib/loader.js:187:9)
at readDocument (/node_modules/.pnpm/js-yaml@4.1.0/node_modules/js-yaml/lib/loader.js:1645:5)
at loadDocuments (/node_modules/.pnpm/js-yaml@4.1.0/node_modules/js-yaml/lib/loader.js:1688:5)
at Object.load (/node_modules/.pnpm/js-yaml@4.1.0/node_modules/js-yaml/lib/loader.js:1714:19)
at loadLegacyConfigFile (/node_modules/.pnpm/@eslint+eslintrc@2.1.4/node_modules/@eslint/eslintrc/dist/eslintrc.cjs:2565:21)
at loadConfigFile (/node_modules/.pnpm/@eslint+eslintrc@2.1.4/node_modules/@eslint/eslintrc/dist/eslintrc.cjs:2680:20)
at ConfigArrayFactory._loadConfigData (/node_modules/.pnpm/@eslint+eslintrc@2.1.4/node_modules/@eslint/eslintrc/dist/eslintrc.cjs:2984:42)
at ConfigArrayFactory.loadFile (/node_modules/.pnpm/@eslint+eslintrc@2.1.4/node_modules/@eslint/eslintrc/dist/eslintrc.cjs:2850:40)
at createCLIConfigArray (/node_modules/.pnpm/@eslint+eslintrc@2.1.4/node_modules/@eslint/eslintrc/dist/eslintrc.cjs:3660:35)

apps/roam/src/components/AdvancedNodeSearchDialog/utils.ts

Oops! Something went wrong! :(

ESLint: 8.57.1

YAMLException: Cannot read config file: /apps/roam/eslint.config.mjs
Error: end of the stream or a document separator is expected (8:20)

5 | export default [
6 | ...config,
7 | {
8 | languageOptions: {
------------------------^
9 | parserOptions: {
10 | tsconfigRootDir: ".",
at generateError (/node_modules/.pnpm/js-yaml@4.1.0/node_modules/js-yaml/lib/loader.js:183:10)
at throwError (/node_modules/.pnpm/js-yaml@4.1.0/node_modules/js-yaml/lib/loader.js:187:9)
at readDocument (/node_modules/.pnpm/js-yaml@4.1.0/node_modules/js-yaml/lib/loader.js:1645:5)
at loadDocuments (/node_modules/.pnpm/js-yaml@4.1.0/node_modules/js-yaml/lib/loader.js:1688:5)
at Object.load (/node_modules/.pnpm/js-yaml@4.1.0/node_modules/js-yaml/lib/loader.js:1714:19)
at loadLegacyConfigFile (/node_modules/.pnpm/@eslint+eslintrc@2.1.4/node_modules/@eslint/eslintrc/dist/eslintrc.cjs:2565:21)
at loadConfigFile (/node_modules/.pnpm/@eslint+eslintrc@2.1.4/node_modules/@eslint/eslintrc/dist/eslintrc.cjs:2680:20)
at ConfigArrayFactory._loadConfigData (/node_modules/.pnpm/@eslint+eslintrc@2.1.4/node_modules/@eslint/eslintrc/dist/eslintrc.cjs:2984:42)
at ConfigArrayFactory.loadFile (/node_modules/.pnpm/@eslint+eslintrc@2.1.4/node_modules/@eslint/eslintrc/dist/eslintrc.cjs:2850:40)
at createCLIConfigArray (/node_modules/.pnpm/@eslint+eslintrc@2.1.4/node_modules/@eslint/eslintrc/dist/eslintrc.cjs:3660:35)

apps/roam/src/utils/discourseNodeSearchProviders.ts

Oops! Something went wrong! :(

ESLint: 8.57.1

YAMLException: Cannot read config file: /apps/roam/eslint.config.mjs
Error: end of the stream or a document separator is expected (8:20)

5 | export default [
6 | ...config,
7 | {
8 | languageOptions: {
------------------------^
9 | parserOptions: {
10 | tsconfigRootDir: ".",
at generateError (/node_modules/.pnpm/js-yaml@4.1.0/node_modules/js-yaml/lib/loader.js:183:10)
at throwError (/node_modules/.pnpm/js-yaml@4.1.0/node_modules/js-yaml/lib/loader.js:187:9)
at readDocument (/node_modules/.pnpm/js-yaml@4.1.0/node_modules/js-yaml/lib/loader.js:1645:5)
at loadDocuments (/node_modules/.pnpm/js-yaml@4.1.0/node_modules/js-yaml/lib/loader.js:1688:5)
at Object.load (/node_modules/.pnpm/js-yaml@4.1.0/node_modules/js-yaml/lib/loader.js:1714:19)
at loadLegacyConfigFile (/node_modules/.pnpm/@eslint+eslintrc@2.1.4/node_modules/@eslint/eslintrc/dist/eslintrc.cjs:2565:21)
at loadConfigFile (/node_modules/.pnpm/@eslint+eslintrc@2.1.4/node_modules/@eslint/eslintrc/dist/eslintrc.cjs:2680:20)
at ConfigArrayFactory._loadConfigData (/node_modules/.pnpm/@eslint+eslintrc@2.1.4/node_modules/@eslint/eslintrc/dist/eslintrc.cjs:2984:42)
at ConfigArrayFactory.loadFile (/node_modules/.pnpm/@eslint+eslintrc@2.1.4/node_modules/@eslint/eslintrc/dist/eslintrc.cjs:2850:40)
at createCLIConfigArray (/node_modules/.pnpm/@eslint+eslintrc@2.1.4/node_modules/@eslint/eslintrc/dist/eslintrc.cjs:3660:35)

  • 2 others

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

🧹 Nitpick comments (2)
apps/roam/src/utils/discourseNodeSearchTypes.ts (1)

39-73: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Undocumented fallback limitation and inconsistent placeholder defaults.

When uid isn't found in resultsByUid, the placeholder SearchResult silently drops excerpt, createdAt, and lastModified (empty strings) but sets authorName to "Unknown" instead of "". This inconsistency could surface oddly in the UI (e.g., "Unknown" author next to blank dates), and the fact that this fallback path exists at all (and why real fields are unavailable) isn't documented anywhere.

As per coding guidelines, **/*.{ts,tsx,js,jsx,md,mdx} files should "Document limitations, known bugs, or edge cases where behavior may not align with expectations."

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@apps/roam/src/utils/discourseNodeSearchTypes.ts` around lines 39 - 73, The
semantic fallback in toScoredSearchResultFromSemantic creates a partial
SearchResult when resultsByUid has no match, but its placeholder defaults are
inconsistent and the limitation is undocumented. Update the fallback in
toScoredSearchResultFromSemantic so the placeholder fields use a consistent
convention, and add a brief comment/doc note near this path explaining that the
data is incomplete because no indexed result was found and which fields are
intentionally unavailable.

Source: Coding guidelines

apps/roam/src/components/AdvancedNodeSearchDialog/useAdvancedNodeSearchResults.ts (1)

71-75: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Reuse excludeDefaultNodes instead of duplicating the backedBy filter.

getDiscourseNodes.ts already exports excludeDefaultNodes for exactly this "drop default-backed nodes" check. Inlining node.backedBy === "user" here duplicates that logic and risks drifting if the definition of "default" changes.

♻️ Reuse existing utility
-import getDiscourseNodes from "~/utils/getDiscourseNodes";
+import getDiscourseNodes, { excludeDefaultNodes } from "~/utils/getDiscourseNodes";
...
     const discourseNodes = getDiscourseNodes().filter(
-      (node) =>
-        node.backedBy === "user" &&
-        (!typeFilter || typeFilter.includes(node.type)),
+      (node) =>
+        excludeDefaultNodes(node) &&
+        (!typeFilter || typeFilter.includes(node.type)),
     );
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In
`@apps/roam/src/components/AdvancedNodeSearchDialog/useAdvancedNodeSearchResults.ts`
around lines 71 - 75, The discourse node filtering in
useAdvancedNodeSearchResults duplicates the default-node check by inlining
node.backedBy === "user". Update the discourseNodes computation to reuse the
existing excludeDefaultNodes helper exported from getDiscourseNodes.ts, then
keep the typeFilter condition on top of that shared utility so the filtering
logic stays consistent and centralized.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@apps/roam/src/utils/discourseNodeSearchTypes.ts`:
- Around line 20-37: The combineSemanticAndMiniSearchResults helper currently
seeds seenUids from semantic but does not deduplicate semantic entries
themselves, so repeated uid values can still appear in the returned list. Update
combineSemanticAndMiniSearchResults to filter the semantic array with the same
seenUids logic used for miniSearch before building combined, keeping only the
first ScoredSearchResult per uid.

In `@apps/roam/src/utils/searchDiscourseNodes.ts`:
- Around line 21-69: The MiniSearch fallback in searchDiscourseNodes is not
protected, so a thrown error from runMiniSearch() can still reject the search
update. Update searchDiscourseNodes to invoke runMiniSearch through a safe
wrapper in both the isRoamSemanticSearchEnabled() disabled branch and the
catch/combine fallback path, and return an empty array if MiniSearch fails so
the function always degrades cleanly.

---

Nitpick comments:
In
`@apps/roam/src/components/AdvancedNodeSearchDialog/useAdvancedNodeSearchResults.ts`:
- Around line 71-75: The discourse node filtering in
useAdvancedNodeSearchResults duplicates the default-node check by inlining
node.backedBy === "user". Update the discourseNodes computation to reuse the
existing excludeDefaultNodes helper exported from getDiscourseNodes.ts, then
keep the typeFilter condition on top of that shared utility so the filtering
logic stays consistent and centralized.

In `@apps/roam/src/utils/discourseNodeSearchTypes.ts`:
- Around line 39-73: The semantic fallback in toScoredSearchResultFromSemantic
creates a partial SearchResult when resultsByUid has no match, but its
placeholder defaults are inconsistent and the limitation is undocumented. Update
the fallback in toScoredSearchResultFromSemantic so the placeholder fields use a
consistent convention, and add a brief comment/doc note near this path
explaining that the data is incomplete because no indexed result was found and
which fields are intentionally unavailable.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Repository UI

Review profile: CHILL

Plan: Pro

Run ID: 0b498032-11de-469b-b587-52f3e638f6c8

📥 Commits

Reviewing files that changed from the base of the PR and between 4d4ddff and 7acc534.

📒 Files selected for processing (5)
  • apps/roam/src/components/AdvancedNodeSearchDialog/useAdvancedNodeSearchResults.ts
  • apps/roam/src/components/AdvancedNodeSearchDialog/utils.ts
  • apps/roam/src/utils/discourseNodeSearchProviders.ts
  • apps/roam/src/utils/discourseNodeSearchTypes.ts
  • apps/roam/src/utils/searchDiscourseNodes.ts

Comment on lines +20 to +37
export const combineSemanticAndMiniSearchResults = ({
semantic,
miniSearch,
}: {
semantic: ScoredSearchResult[];
miniSearch: ScoredSearchResult[];
}): ScoredSearchResult[] => {
const seenUids = new Set(semantic.map((entry) => entry.result.uid));
const combined = [...semantic];

miniSearch.forEach((entry) => {
if (seenUids.has(entry.result.uid)) return;
seenUids.add(entry.result.uid);
combined.push(entry);
});

return combined;
};

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail

printf '\n== File outline ==\n'
ast-grep outline apps/roam/src/utils/discourseNodeSearchTypes.ts --view expanded || true

printf '\n== File contents ==\n'
cat -n apps/roam/src/utils/discourseNodeSearchTypes.ts

printf '\n== Search usages ==\n'
rg -n "combineSemanticAndMiniSearchResults|ScoredSearchResult|semantic.*result.uid|miniSearch" apps/roam/src -S

Repository: DiscourseGraphs/discourse-graph

Length of output: 8447


🏁 Script executed:

#!/bin/bash
set -euo pipefail

printf '\n== searchDiscourseNodes ==\n'
cat -n apps/roam/src/utils/searchDiscourseNodes.ts

printf '\n== Advanced search semantic path ==\n'
sed -n '250,320p' apps/roam/src/components/AdvancedNodeSearchDialog/utils.ts

printf '\n== useAdvancedNodeSearchResults ==\n'
sed -n '1,140p' apps/roam/src/components/AdvancedNodeSearchDialog/useAdvancedNodeSearchResults.ts

printf '\n== semantic provider result shaping ==\n'
sed -n '330,420p' apps/roam/src/utils/discourseNodeSearchProviders.ts

Repository: DiscourseGraphs/discourse-graph

Length of output: 9516


🏁 Script executed:

#!/bin/bash
set -euo pipefail

printf '\n== runRoamSemanticSearch definition ==\n'
rg -n "export .*runRoamSemanticSearch|const runRoamSemanticSearch|function runRoamSemanticSearch" apps/roam/src/utils/discourseNodeSearchProviders.ts apps/roam/src -S

printf '\n== relevant provider section ==\n'
sed -n '480,620p' apps/roam/src/utils/discourseNodeSearchProviders.ts

Repository: DiscourseGraphs/discourse-graph

Length of output: 3341


🏁 Script executed:

#!/bin/bash
set -euo pipefail

sed -n '220,320p' apps/roam/src/utils/discourseNodeSearchProviders.ts

printf '\n== filterResultsToDiscourseNodeTitles ==\n'
rg -n "filterResultsToDiscourseNodeTitles" apps/roam/src/utils/discourseNodeSearchProviders.ts apps/roam/src -n -A 80 -B 20

Repository: DiscourseGraphs/discourse-graph

Length of output: 46659


🌐 Web query:

Roam Alpha API semanticSearch returns duplicate page hits unique uid contract

💡 Result:

The Roam Alpha API provides a semanticSearch functionality, which is separate from the standard Datalog-based q query API [1][2][3]. If you are encountering duplicate page hits when using this function, it is generally not a violation of the underlying UID contract, as Roam's database architecture relies on unique UIDs for all entities (blocks and pages) [3]. Technical considerations regarding this behavior include: 1. Data Normalization and Duplication: The semantic search index may occasionally include multiple references or "hits" that resolve to the same underlying entity UID due to the nature of how content is chunked or indexed by the semantic engine [4]. If your application logic expects a one-to-one mapping between search hits and unique entities, you should implement client-side deduplication using the unique uid (or block-uid) field present in the result object [1][5][3]. 2. Querying Alternatives: If semanticSearch is returning redundant data that complicates your integration, you can use the more deterministic Datalog query interface (roamAlphaAPI.q) [2][6][3]. Datalog queries allow you to use the distinct keyword or explicitly group results by UID, ensuring that you only retrieve a single, unique record for each entity regardless of how many blocks or page references match your criteria [3]. 3. API Contracts: There is no formal "unique hit" contract for the semantic search API that guarantees a distinct list of results [1]. The API is intended to provide relevant search context, which may include multiple fragments of a single page if they meet the semantic threshold. Always rely on the uid property as the primary key for identifying and de-duplicating entities within your application [5][3]. If you are building an extension or integration, verify that you are checking window.roamAlphaAPI.data.semanticSearchEnabled() before invocation to ensure the environment is correctly initialized [1].

Citations:


Dedup semantic hits before combining The Roam semantic path still passes repeated uids through, so duplicate semantic hits can appear twice in the final list. Dedup semantic with the same seenUids pass used for miniSearch.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@apps/roam/src/utils/discourseNodeSearchTypes.ts` around lines 20 - 37, The
combineSemanticAndMiniSearchResults helper currently seeds seenUids from
semantic but does not deduplicate semantic entries themselves, so repeated uid
values can still appear in the returned list. Update
combineSemanticAndMiniSearchResults to filter the semantic array with the same
seenUids logic used for miniSearch before building combined, keeping only the
first ScoredSearchResult per uid.

Comment on lines +21 to +69
export const searchDiscourseNodes = async ({
nodeTypes,
query,
resultsByUid,
runMiniSearch,
}: {
nodeTypes: DiscourseNode[];
query: string;
resultsByUid: Map<string, SearchResult>;
runMiniSearch: () => ScoredSearchResult[];
}): Promise<ScoredSearchResult[]> => {
const trimmedQuery = query.trim();
if (!trimmedQuery) return [];

if (!isRoamSemanticSearchEnabled()) {
return runMiniSearch();
}

try {
const providerResult = await runRoamSemanticSearch({
nodeTypes,
query: trimmedQuery,
});
const semanticResults = providerResult.filteredResults.map((item) =>
toScoredSearchResultFromSemantic({
uid: item.uid,
title: item.text,
type: item.type,
nodeTypeLabel: item.nodeTypeLabel,
score: item.score ?? 0,
resultsByUid,
}),
);

if (
providerResult.filteredResultCount >=
SEMANTIC_SEARCH_MIN_DISCOURSE_RESULTS
) {
return semanticResults;
}

return combineSemanticAndMiniSearchResults({
semantic: semanticResults,
miniSearch: runMiniSearch(),
});
} catch {
return runMiniSearch();
}
};

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🩺 Stability & Availability | 🟡 Minor | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Locate the relevant search utilities and call sites.
git ls-files 'apps/roam/src/**/*search*' 'apps/roam/src/**/*Search*' 'apps/roam/src/**/*mini*' | sed -n '1,200p'

printf '\n--- searchDiscourseNodes.ts ---\n'
cat -n apps/roam/src/utils/searchDiscourseNodes.ts | sed -n '1,220p'

printf '\n--- useAdvancedNodeSearchResults references ---\n'
rg -n "useAdvancedNodeSearchResults|searchDiscourseNodes|runMiniSearch|MiniSearch" apps/roam/src -A 3 -B 3

Repository: DiscourseGraphs/discourse-graph

Length of output: 29073


🏁 Script executed:

#!/bin/bash
set -euo pipefail

printf '\n--- AdvancedNodeSearchDialog/utils.ts (search helper) ---\n'
cat -n apps/roam/src/components/AdvancedNodeSearchDialog/utils.ts | sed -n '240,340p'

printf '\n--- AdvancedNodeSearchDialog/useAdvancedNodeSearchResults.ts (fallback path) ---\n'
cat -n apps/roam/src/components/AdvancedNodeSearchDialog/useAdvancedNodeSearchResults.ts | sed -n '70,110p'

printf '\n--- MiniSearch search-error handling in the repo ---\n'
rg -n "try \\{|catch \\{|miniSearch\\.search\\(|search\\(MiniSearch\\.wildcard|searchTerm" apps/roam/src/components/AdvancedNodeSearchDialog apps/roam/src/utils -A 3 -B 3

Repository: DiscourseGraphs/discourse-graph

Length of output: 50387


🌐 Web query:

MiniSearch search() throws invalid query syntax malformed query exception documentation

💡 Result:

MiniSearch does not explicitly throw a "malformed query" exception or a generic "invalid query syntax" error in its official documentation or API. Instead, errors encountered during the search process typically manifest as JavaScript runtime exceptions—such as TypeErrors—triggered when the search() method receives unexpected input types or structures [1][2]. Common causes for these runtime errors include: 1. Wildcard Symbol Misalignment: If you use MiniSearch.wildcard, errors can occur if there is a mismatch in the MiniSearch instance or package version importing the symbol versus the instance performing the search [1][2]. This often happens in environments like Next.js or edge runtime setups where multiple instances of the library might be active, causing the MiniSearch.wildcard check to fail and the engine to incorrectly process the input as a different type of query [1][2]. 2. Incorrect Query Expression Tree: When passing an object as a query (an expression tree) rather than a string, the structure must strictly adhere to the Query type, which expects queries (an array of subqueries) and a combineWith operator [3][4][5]. Providing a malformed object that lacks these required properties or passes invalid types to the queries field will lead to runtime errors when the internal executeQuery method attempts to map or iterate over them [6][1][3]. If you are experiencing search-time errors, verify the following: - Ensure the input is either a plain string or a correctly formatted query expression tree object (containing a queries array and a combineWith field) [4][5]. - If using MiniSearch.wildcard, ensure it is being imported from the same package bundle that initialized the MiniSearch instance [1][2]. - Check your environment for potential multiple-instance issues (e.g., singleton patterns or conflicting dependency versions) if running in complex frameworks like Next.js [1]. For further debugging, inspect the stack trace to see if the error occurs within executeQuery or executeQuerySpec, as these are the primary internal methods where query processing occurs [6][3].

Citations:


Guard the MiniSearch fallback too
runMiniSearch() is called directly in the disabled-search branch and again in the fallback path. If that helper ever throws, the recovery path throws as well and the search result update can still reject; wrap it in a safe fallback so this degrades to [] instead.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@apps/roam/src/utils/searchDiscourseNodes.ts` around lines 21 - 69, The
MiniSearch fallback in searchDiscourseNodes is not protected, so a thrown error
from runMiniSearch() can still reject the search update. Update
searchDiscourseNodes to invoke runMiniSearch through a safe wrapper in both the
isRoamSemanticSearchEnabled() disabled branch and the catch/combine fallback
path, and return an empty array if MiniSearch fails so the function always
degrades cleanly.

@mdroidian mdroidian left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think the semantic result ordering needs another pass.

Right now we take Roam semantic search results, filter them to discourse-node pages, then sort semantic results by score in sortSearchResults.

I think we should preserve Roam’s returned order for semantic results. A clean fix would be to store a semanticRank/providerRank when mapping providerResult.filteredResults, then for relevance sorting:

  • semantic results stay before MiniSearch results
  • semantic-vs-semantic uses Roam return order
  • MiniSearch-vs-MiniSearch uses MiniSearch score
  • combined sparse fallback still dedupes by uid

That would better match the user expectation that Roam semantic search’s own ordering is the relevance ordering.

comparison = compareNumbers(aEntry.score, bEntry.score, sort.direction);
break;
case "alphabetical":
comparison = compareStrings(

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Looks like this is doing some odd scoring even when roam's semantic search returns properly scored results:

Ours:
Image

Roam's:

Image

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants