Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions apps/desktop/src-tauri/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@
use ext::*;
use store::*;

use tauri::Manager;

Check warning on line 11 in apps/desktop/src-tauri/src/lib.rs

View workflow job for this annotation

GitHub Actions / desktop_ci (linux, depot-ubuntu-22.04-8)

unused import: `tauri::Manager`

Check warning on line 11 in apps/desktop/src-tauri/src/lib.rs

View workflow job for this annotation

GitHub Actions / desktop_ci (linux, depot-ubuntu-22.04-8)

unused import: `tauri::Manager`
use tauri_plugin_permissions::{Permission, PermissionsPluginExt};
use tauri_plugin_windows::{AppWindow, WindowsPluginExt};

Expand Down Expand Up @@ -120,6 +120,7 @@
.plugin(tauri_plugin_window_state::Builder::default().build())
.plugin(tauri_plugin_listener::init())
.plugin(tauri_plugin_listener2::init())
.plugin(tauri_plugin_tantivy::init())
.plugin(tauri_plugin_audio_priority::init())
.plugin(tauri_plugin_local_stt::init(
tauri_plugin_local_stt::InitOptions {
Expand Down
7 changes: 6 additions & 1 deletion apps/desktop/src/contexts/search/engine/content.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,5 +13,10 @@ export function createSessionSearchableContent(
export function createHumanSearchableContent(
row: Record<string, unknown>,
): string {
return mergeContent([row.email, row.job_title, row.linkedin_username]);
return mergeContent([
row.email,
row.job_title,
row.linkedin_username,
row.memo,
]);
}
38 changes: 15 additions & 23 deletions apps/desktop/src/contexts/search/engine/filters.ts
Original file line number Diff line number Diff line change
@@ -1,31 +1,23 @@
import type { SearchFilters as TantivySearchFilters } from "@hypr/plugin-tantivy";

import type { SearchFilters } from "./types";

export function buildOramaFilters(
export function buildTantivyFilters(
filters: SearchFilters | null,
): Record<string, any> | undefined {
): TantivySearchFilters | undefined {
if (!filters || !filters.created_at) {
return undefined;
}

const createdAtConditions: Record<string, number> = {};

if (filters.created_at.gte !== undefined) {
createdAtConditions.gte = filters.created_at.gte;
}
if (filters.created_at.lte !== undefined) {
createdAtConditions.lte = filters.created_at.lte;
}
if (filters.created_at.gt !== undefined) {
createdAtConditions.gt = filters.created_at.gt;
}
if (filters.created_at.lt !== undefined) {
createdAtConditions.lt = filters.created_at.lt;
}
if (filters.created_at.eq !== undefined) {
createdAtConditions.eq = filters.created_at.eq;
}

return Object.keys(createdAtConditions).length > 0
? { created_at: createdAtConditions }
: undefined;
return {
created_at: {
gte: filters.created_at.gte ?? null,
lte: filters.created_at.lte ?? null,
gt: filters.created_at.gt ?? null,
lt: filters.created_at.lt ?? null,
eq: filters.created_at.eq ?? null,
},
doc_type: null,
facet: null,
};
}
80 changes: 29 additions & 51 deletions apps/desktop/src/contexts/search/engine/index.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,3 @@
import { create, search as oramaSearch } from "@orama/orama";
import { pluginQPS } from "@orama/plugin-qps";
import {
createContext,
useCallback,
Expand All @@ -9,17 +7,18 @@ import {
useState,
} from "react";

import { commands as tantivy } from "@hypr/plugin-tantivy";

import { type Store as MainStore } from "../../../store/tinybase/store/main";
import { buildOramaFilters } from "./filters";
import { buildTantivyFilters } from "./filters";
import { indexHumans, indexOrganizations, indexSessions } from "./indexing";
import {
createHumanListener,
createOrganizationListener,
createSessionListener,
} from "./listeners";
import type { Index, SearchFilters, SearchHit } from "./types";
import { SEARCH_SCHEMA } from "./types";
import { normalizeQuery, parseQuery } from "./utils";
import type { SearchEntityType, SearchFilters, SearchHit } from "./types";
import { normalizeQuery } from "./utils";

export type {
SearchDocument,
Expand All @@ -44,7 +43,6 @@ export function SearchEngineProvider({
store?: MainStore;
}) {
const [isIndexing, setIsIndexing] = useState(true);
const oramaInstance = useRef<Index | null>(null);
const listenerIds = useRef<string[]>([]);

useEffect(() => {
Expand All @@ -56,31 +54,24 @@ export function SearchEngineProvider({
setIsIndexing(true);

try {
const db = create({
schema: SEARCH_SCHEMA,
plugins: [pluginQPS()],
});

indexSessions(db, store);
indexHumans(db, store);
indexOrganizations(db, store);

oramaInstance.current = db;
await indexSessions(store);
await indexHumans(store);
await indexOrganizations(store);

const listener1 = store.addRowListener(
"sessions",
null,
createSessionListener(oramaInstance.current),
createSessionListener(),
);
const listener2 = store.addRowListener(
"humans",
null,
createHumanListener(oramaInstance.current),
createHumanListener(),
);
const listener3 = store.addRowListener(
"organizations",
null,
createOrganizationListener(oramaInstance.current),
createOrganizationListener(),
);

listenerIds.current = [listener1, listener2, listener3];
Expand All @@ -106,43 +97,30 @@ export function SearchEngineProvider({
query: string,
filters: SearchFilters | null = null,
): Promise<SearchHit[]> => {
if (!oramaInstance.current) {
return [];
}

const normalizedQuery = normalizeQuery(query);
const tantivyFilters = buildTantivyFilters(filters);

try {
const whereClause = buildOramaFilters(filters);

if (normalizedQuery.length < 1) {
const searchResults = await oramaSearch(oramaInstance.current, {
term: "",
sortBy: {
property: "created_at",
order: "DESC",
},
limit: 10,
...(whereClause && { where: whereClause }),
});
return searchResults.hits as SearchHit[];
}
const result = await tantivy.search({
query: normalizedQuery,
filters: tantivyFilters,
});

const parsed = parseQuery(query);
if (result.status === "error") {
console.error("Search failed:", result.error);
return [];
}

const searchResults = await oramaSearch(oramaInstance.current, {
term: parsed.term,
exact: parsed.exact,
boost: {
title: 3,
content: 1,
return result.data.hits.map((hit) => ({
score: hit.score,
document: {
id: hit.document.id,
type: hit.document.doc_type as SearchEntityType,
title: hit.document.title,
content: hit.document.content,
created_at: hit.document.created_at,
},
limit: 100,
tolerance: parsed.exact ? 0 : 1,
...(whereClause && { where: whereClause }),
});

return searchResults.hits as SearchHit[];
}));
} catch (error) {
console.error("Search failed:", error);
return [];
Expand Down
46 changes: 35 additions & 11 deletions apps/desktop/src/contexts/search/engine/indexing.ts
Original file line number Diff line number Diff line change
@@ -1,19 +1,18 @@
import { insert } from "@orama/orama";
import { type SearchDocument, commands as tantivy } from "@hypr/plugin-tantivy";

import { type Store as MainStore } from "../../../store/tinybase/store/main";
import {
createHumanSearchableContent,
createSessionSearchableContent,
} from "./content";
import type { Index } from "./types";
import {
collectCells,
collectEnhancedNotesContent,
toEpochMs,
toTrimmedString,
} from "./utils";

export function indexSessions(db: Index, store: MainStore): void {
export async function indexSessions(store: MainStore): Promise<void> {
const fields = [
"user_id",
"created_at",
Expand All @@ -24,58 +23,83 @@ export function indexSessions(db: Index, store: MainStore): void {
"transcript",
];

const documents: SearchDocument[] = [];

store.forEachRow("sessions", (rowId: string, _forEachCell) => {
const row = collectCells(store, "sessions", rowId, fields);
row.enhanced_notes_content = collectEnhancedNotesContent(store, rowId);
const title = toTrimmedString(row.title) || "Untitled";

void insert(db, {
documents.push({
id: rowId,
type: "session",
doc_type: "session",
language: null,
title,
content: createSessionSearchableContent(row),
created_at: toEpochMs(row.created_at),
facets: [],
});
});

if (documents.length > 0) {
await tantivy.updateDocuments(documents, null);
}
}

export function indexHumans(db: Index, store: MainStore): void {
export async function indexHumans(store: MainStore): Promise<void> {
const fields = [
"name",
"email",
"org_id",
"job_title",
"linkedin_username",
"created_at",
"memo",
];

const documents: SearchDocument[] = [];

store.forEachRow("humans", (rowId: string, _forEachCell) => {
const row = collectCells(store, "humans", rowId, fields);
const title = toTrimmedString(row.name) || "Unknown";

void insert(db, {
documents.push({
id: rowId,
type: "human",
doc_type: "human",
language: null,
title,
content: createHumanSearchableContent(row),
created_at: toEpochMs(row.created_at),
facets: [],
});
});

if (documents.length > 0) {
await tantivy.updateDocuments(documents, null);
}
}

export function indexOrganizations(db: Index, store: MainStore): void {
export async function indexOrganizations(store: MainStore): Promise<void> {
const fields = ["name", "created_at"];

const documents: SearchDocument[] = [];

store.forEachRow("organizations", (rowId: string, _forEachCell) => {
const row = collectCells(store, "organizations", rowId, fields);
const title = toTrimmedString(row.name) || "Unknown Organization";

void insert(db, {
documents.push({
id: rowId,
type: "organization",
doc_type: "organization",
language: null,
title,
content: "",
created_at: toEpochMs(row.created_at),
facets: [],
});
});

if (documents.length > 0) {
await tantivy.updateDocuments(documents, null);
}
}
Loading
Loading