Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Typed hits property in search method #21

Merged
merged 10 commits into from
Jul 17, 2022
39 changes: 26 additions & 13 deletions packages/lyra/src/lyra.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,10 @@ export type LyraProperties<
defaultLanguage?: Language;
};

export type LyraDocs = Map<string, object>;
export type LyraDocs<TDoc extends PropertiesSchema> = Map<
string,
ResolveSchema<TDoc>
>;

export type SearchParams = {
term: string;
Expand All @@ -34,25 +37,29 @@ export type SearchParams = {

type LyraIndex = Map<string, Trie>;

type QueueDocParams = {
type QueueDocParams<TTSchema extends PropertiesSchema> = {
id: string;
doc: object;
doc: ResolveSchema<TTSchema>;
language: Language;
};

type SearchResult = Promise<{
type SearchResult<TTSchema extends PropertiesSchema> = Promise<{
count: number;
hits: object[];
hits: RetrievedDoc<TTSchema>[];
elapsed: string;
}>;

type RetrievedDoc<TDoc extends PropertiesSchema> = ResolveSchema<TDoc> & {
id: string;
};

export class Lyra<TSchema extends PropertiesSchema = PropertiesSchema> {
private defaultLanguage: Language = "english";
private schema: TSchema;
private docs: LyraDocs = new Map();
private docs: LyraDocs<TSchema> = new Map();
private index: LyraIndex = new Map();

private queue: queueAsPromised<QueueDocParams> = fastq.promise(
private queue: queueAsPromised<QueueDocParams<TSchema>> = fastq.promise(
this,
this._insert,
1
Expand Down Expand Up @@ -91,11 +98,13 @@ export class Lyra<TSchema extends PropertiesSchema = PropertiesSchema> {
async search(
params: SearchParams,
language: Language = this.defaultLanguage
): SearchResult {
): SearchResult<TSchema> {
const tokens = tokenize(params.term, language).values();
const indices = this.getIndices(params.properties);
const { limit = 10, offset = 0, exact = false } = params;
const results: object[] = new Array({ length: limit });
const results: RetrievedDoc<TSchema>[] = Array.from({
length: limit,
});
let totalResults = 0;

const timeStart = getNanosecondsTime();
Expand Down Expand Up @@ -129,7 +138,7 @@ export class Lyra<TSchema extends PropertiesSchema = PropertiesSchema> {
break;
}

const fullDoc = this.docs.get(id);
const fullDoc = this.docs.get(id)!;
Copy link
Contributor Author

Choose a reason for hiding this comment

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

@micheleriva Is there a better way to do that? I hate asserting non-null

Copy link
Member

Choose a reason for hiding this comment

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

mmm afaik this is the only way... if there's a guarantee that the value exists, it's not that bad

results[i] = { id, ...fullDoc };
i++;
}
Expand Down Expand Up @@ -233,7 +242,11 @@ export class Lyra<TSchema extends PropertiesSchema = PropertiesSchema> {
return { id };
}

private async _insert({ doc, id, language }: QueueDocParams): Promise<void> {
private async _insert({
doc,
id,
language,
}: QueueDocParams<TSchema>): Promise<void> {
const index = this.index;
this.docs.set(id, doc);

Expand All @@ -260,10 +273,10 @@ export class Lyra<TSchema extends PropertiesSchema = PropertiesSchema> {
}

private async checkInsertDocSchema(
doc: QueueDocParams["doc"]
doc: QueueDocParams<TSchema>["doc"]
): Promise<boolean> {
function recursiveCheck(
newDoc: QueueDocParams["doc"],
newDoc: QueueDocParams<TSchema>["doc"],
schema: PropertiesSchema
): boolean {
for (const key in newDoc) {
Expand Down
36 changes: 18 additions & 18 deletions packages/lyra/tests/lyra.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -103,7 +103,7 @@ describe("lyra", () => {
expect(ex1Insert.id).toBeDefined();
expect(ex1Search.count).toBe(1);
expect(ex1Search.elapsed).toBeDefined();
expect((ex1Search.hits[0] as any).example).toBe("The quick, brown, fox");
expect(ex1Search.hits[0].example).toBe("The quick, brown, fox");
});

it("should correctly paginate results", async () => {
Expand All @@ -126,17 +126,17 @@ describe("lyra", () => {
const search4 = await db.search({ term: "f", limit: 2, offset: 2 });

expect(search1.count).toBe(4);
expect((search1.hits[0] as any).animal).toBe("Quick brown fox");
expect(search1.hits[0].animal).toBe("Quick brown fox");

expect(search2.count).toBe(4);
expect((search2.hits[0] as any).animal).toBe("Fast chicken");
expect(search2.hits[0].animal).toBe("Fast chicken");

expect(search3.count).toBe(4);
expect((search3.hits[0] as any).animal).toBe("Fabolous ducks");
expect(search3.hits[0].animal).toBe("Fabolous ducks");

expect(search4.count).toBe(4);
expect((search4.hits[0] as any).animal).toBe("Fabolous ducks");
expect((search4.hits[1] as any).animal).toBe("Fantastic horse");
expect(search4.hits[0].animal).toBe("Fabolous ducks");
expect(search4.hits[1].animal).toBe("Fantastic horse");
});

it("Should throw an error when searching in non-existing indices", async () => {
Expand Down Expand Up @@ -187,11 +187,11 @@ describe("lyra", () => {

expect(res).toBeTruthy();
expect(searchResult.count).toBe(1);
expect((searchResult.hits[0] as any).author).toBe("Oscar Wilde");
expect((searchResult.hits[0] as any).quote).toBe(
expect(searchResult.hits[0].author).toBe("Oscar Wilde");
expect(searchResult.hits[0].quote).toBe(
"To live is the rarest thing in the world. Most people exist, that is all."
);
expect((searchResult.hits[0] as any).id).toBe(id2);
expect(searchResult.hits[0].id).toBe(id2);
});

it("Should preserve identical docs after deletion", async () => {
Expand Down Expand Up @@ -231,18 +231,18 @@ describe("lyra", () => {

expect(res).toBeTruthy();
expect(searchResult.count).toBe(1);
expect((searchResult.hits[0] as any).author).toBe("Oscar Wilde");
expect((searchResult.hits[0] as any).quote).toBe(
expect(searchResult.hits[0].author).toBe("Oscar Wilde");
expect(searchResult.hits[0].quote).toBe(
"Be yourself; everyone else is already taken."
);
expect((searchResult.hits[0] as any).id).toBe(id2);
expect(searchResult.hits[0].id).toBe(id2);

expect(searchResult2.count).toBe(1);
expect((searchResult2.hits[0] as any).author).toBe("Oscar Wilde");
expect((searchResult2.hits[0] as any).quote).toBe(
expect(searchResult2.hits[0].author).toBe("Oscar Wilde");
expect(searchResult2.hits[0].quote).toBe(
"Be yourself; everyone else is already taken."
);
expect((searchResult2.hits[0] as any).id).toBe(id2);
expect(searchResult2.hits[0].id).toBe(id2);
});

it("Should be able to insert documens with non-searchable fields", async () => {
Expand Down Expand Up @@ -274,7 +274,7 @@ describe("lyra", () => {
});

expect(search.count).toBe(1);
expect((search.hits[0] as any).author).toBe("Frank Zappa");
expect(search.hits[0].author).toBe("Frank Zappa");
});

it("Should exact match", async () => {
Expand Down Expand Up @@ -303,10 +303,10 @@ describe("lyra", () => {
});

expect(exactSearch.count).toBe(1);
expect((exactSearch.hits[0] as any).quote).toBe(
expect(exactSearch.hits[0].quote).toBe(
"Be yourself; everyone else is already taken."
);
expect((exactSearch.hits[0] as any).author).toBe("Oscar Wilde");
expect(exactSearch.hits[0].author).toBe("Oscar Wilde");
});

it("Shouldn't tolerate typos", async () => {
Expand Down