mirror of
https://github.com/tauri-apps/tauri-search.git
synced 2026-02-04 02:41:20 +01:00
chore: model config API accepts mappers but API still needs finishing
This commit is contained in:
7
docs/src/components.d.ts
vendored
7
docs/src/components.d.ts
vendored
@@ -4,9 +4,12 @@
|
||||
|
||||
declare module 'vue' {
|
||||
export interface GlobalComponents {
|
||||
'AntDesign:fileMarkdownOutlined': typeof import('~icons/ant-design/file-markdown-outlined')['default']
|
||||
'Bx:bxSearchAlt': typeof import('~icons/bx/bx-search-alt')['default']
|
||||
'Carbon:document': typeof import('~icons/carbon/document')['default']
|
||||
'Carbon:errorFilled': typeof import('~icons/carbon/error-filled')['default']
|
||||
'Carbon:listDropdown': typeof import('~icons/carbon/list-dropdown')['default']
|
||||
'Carbon:nextFilled': typeof import('~icons/carbon/next-filled')['default']
|
||||
CarbonLanguage: typeof import('~icons/carbon/language')['default']
|
||||
CarbonMoon: typeof import('~icons/carbon/moon')['default']
|
||||
CarbonSun: typeof import('~icons/carbon/sun')['default']
|
||||
@@ -14,8 +17,10 @@ declare module 'vue' {
|
||||
'Fluent:databaseSearch24Regular': typeof import('~icons/fluent/database-search24-regular')['default']
|
||||
Footer: typeof import('./components/Footer.vue')['default']
|
||||
'Mdi:folderHome': typeof import('~icons/mdi/folder-home')['default']
|
||||
'Mdi:github': typeof import('~icons/mdi/github')['default']
|
||||
'Mdi:languageRust': typeof import('~icons/mdi/language-rust')['default']
|
||||
'Mdi:languageTypescript': typeof import('~icons/mdi/language-typescript')['default']
|
||||
'Ph:linkLight': typeof import('~icons/ph/link-light')['default']
|
||||
README: typeof import('./components/README.md')['default']
|
||||
SearchActions: typeof import('./components/SearchActions.vue')['default']
|
||||
SearchHit: typeof import('./components/SearchHit.vue')['default']
|
||||
@@ -24,6 +29,8 @@ declare module 'vue' {
|
||||
SimpleCard: typeof import('./components/SimpleCard.vue')['default']
|
||||
'Tabler:databaseImport': typeof import('~icons/tabler/database-import')['default']
|
||||
'Teenyicons:dockerOutline': typeof import('~icons/teenyicons/docker-outline')['default']
|
||||
'VscodeIcons:fileTypeRust': typeof import('~icons/vscode-icons/file-type-rust')['default']
|
||||
'VscodeIcons:fileTypeTypescriptOfficial': typeof import('~icons/vscode-icons/file-type-typescript-official')['default']
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -24,32 +24,73 @@ pnpm run watch
|
||||
+++ Ways to Consume this library
|
||||
- **Search Development** - if you are updating docs, index definitions, etc. you'll run this in _watch_ mode (aka., `pnpm run start` (first time) or `pnpm run watch`)
|
||||
- **Deployment** - When an _upstream_ dependency is updated this repo should be trigged by a Netlify build hook. For instance:
|
||||
- `tauri` has a new release to production branch, as a `postbuild` step in Netlify build process, it will call Netlify's API and ask for a rebuild of this repo.
|
||||
- we care about picking up the two AST files to build the API docs (`ts-docs.json`, `rust.json`)
|
||||
- `tauri-docs` releases new docs, again a `postbuild` hook on Netlify is called to it requests a rebuild from this repo
|
||||
- here we need to pickup the directly or MD files
|
||||
- `NPM Dependency` - the `Models` you've defined along with all of the _types_ defined are available as an NPM dependency
|
||||
- `tauri` has a new release to production branch, as a `postbuild` step in Netlify build process, it will call Netlify's API and ask for a rebuild of this repo.
|
||||
- we care about picking up the two AST files to build the API docs (`ts-docs.json`, `rust.json`)
|
||||
- `tauri-docs` releases new docs, again a `postbuild` hook on Netlify is called to it requests a rebuild from this repo
|
||||
- here we need to pickup the directly or MD files
|
||||
- **NPM Dependency** - the `Models` you've defined along with all of the _types_ defined are available as an NPM dependency
|
||||
```ts
|
||||
import { ProseModel } from "tauri-search";
|
||||
import type { MeiliSearchResponse } from "tauri-search";
|
||||
```
|
||||
+++
|
||||
|
||||
## Models
|
||||
|
||||
## Sitemap
|
||||
Central to using this library to build and refresh your search indexes is understanding the concept of `Model`.
|
||||
- A Model has a `1:1` relationship with search indexes (or at least _potential_ indexes)
|
||||
- A Model is intended to represent:
|
||||
- the **document structure** that will be used for docs in the index
|
||||
- allows for **configuring the index** itself (e.g., stop words, synonyms, etc.)
|
||||
- allows you to embed data mappers which map from one document structure to another
|
||||
+++ Take a look at the examples here to get a better bearing:
|
||||
- +++ define the model:
|
||||
```ts
|
||||
/** structure for documents in the Prose index */
|
||||
export interface IProse {
|
||||
title: string; section: string;
|
||||
lastUpdated: number; url: string;
|
||||
}
|
||||
/** structure for the input data structure */
|
||||
export interface IMarkdownAst {
|
||||
h1: string; h2: string; h3: string;
|
||||
url: string;
|
||||
}
|
||||
|
||||
Use the Nav bar at the top to navigation to the various sections:
|
||||
/** create the Prose model */
|
||||
const Prose = createModel("prose", c => c //
|
||||
.synonomys({ js: ["javascript"], javascript: ["js"]})
|
||||
.addMapper("markdown").mapDefn<IMarkdownAst>(i => {
|
||||
title: i.h1,
|
||||
section: i.h2,
|
||||
lastUpdated: i.lastUpdated,
|
||||
url: i.url
|
||||
})
|
||||
)
|
||||
```
|
||||
- +++ use the model to call the MeiliSearch API
|
||||
```ts
|
||||
import { Prose } from "./models";
|
||||
// create an index
|
||||
await Prose.api.createIndex();
|
||||
// get index stats
|
||||
await Prose.api.stats();
|
||||
// search on index
|
||||
await Prose.api.search("foobar");
|
||||
```
|
||||
- +++ leverage mappers embedded in the model
|
||||
```ts
|
||||
import { Prose } from "./models";
|
||||
|
||||
- MeiliSearch Info
|
||||
- Search Bar Info
|
||||
- Docker Info
|
||||
|
||||
Then we move into the core content types:
|
||||
|
||||
- Typescript API Content
|
||||
- Rust API Content
|
||||
- Prose Content
|
||||
// map from Input structure to expected document structure
|
||||
const doc: IProse = Prose.mapWith("markdown", data);
|
||||
|
||||
// using mapping to perform an update on the index
|
||||
await Prose.updateWith("markdown", data);
|
||||
```
|
||||
> note: in the examples we're supposing `data` to be a single Node/Record but
|
||||
> you can actually pass in either a single record or a list and it will manage
|
||||
> both
|
||||
|
||||
## External Resources
|
||||
- General Documentation
|
||||
|
||||
5675
pnpm-lock.yaml
generated
5675
pnpm-lock.yaml
generated
File diff suppressed because it is too large
Load Diff
@@ -1,12 +1,3 @@
|
||||
export enum RankingRule {
|
||||
Words = "Words",
|
||||
Typo = "Typo",
|
||||
Proximity = "Proximity",
|
||||
Attribute = "Attribute",
|
||||
Sort = "Sort",
|
||||
Exactness = "Exactness",
|
||||
}
|
||||
|
||||
export enum TypescriptKind {
|
||||
Project = "Project",
|
||||
Namespace = "Namespace",
|
||||
|
||||
@@ -38,3 +38,5 @@ export const githubMapper = createMapper<GithubRepoResp["data"], IRepoModel>(
|
||||
url: i.html_url as url,
|
||||
})
|
||||
);
|
||||
|
||||
const g = githubMapper.map();
|
||||
|
||||
@@ -146,42 +146,3 @@ export type PropCharacteristicsApi<E extends string> = Omit<
|
||||
},
|
||||
E
|
||||
>;
|
||||
|
||||
|
||||
export type SearchModel<N extends string, T extends any> = {
|
||||
/** the name of the model */
|
||||
name: N;
|
||||
|
||||
info: {
|
||||
/** a unique hash code which represents it's current configuration */
|
||||
hash: `${string}-${string}`;
|
||||
/** all properties and their index attributes */
|
||||
/** the typescript type of the document */
|
||||
type: MorphType<any>;
|
||||
interfaceDefn: string;
|
||||
document: T;
|
||||
index: {
|
||||
stopWords: string[];
|
||||
sortable: string[];
|
||||
displayed: string[];
|
||||
distinct: string[];
|
||||
searchable: string[];
|
||||
filterable: string[];
|
||||
};
|
||||
};
|
||||
|
||||
crud: {
|
||||
/**
|
||||
* Updates the search index with a given document
|
||||
*/
|
||||
update: (id: string, doc: T, endpoint?: string) => Promise<void>;
|
||||
/**
|
||||
* Adds a new document to the index
|
||||
*/
|
||||
add: (doc: T, endpoint?: string) => Promise<void>;
|
||||
|
||||
remove: (doc: T, endpoint?: string) => Promise<void>;
|
||||
|
||||
search: (find: string) => Promise<T[]>;
|
||||
};
|
||||
};
|
||||
|
||||
@@ -3,5 +3,6 @@ export * from "./ts-ast";
|
||||
export * from "./apis";
|
||||
export * from "./type-guards";
|
||||
export * from "./mapping";
|
||||
export * from "./model";
|
||||
export * from "./meiliseach";
|
||||
export * from "./utility";
|
||||
|
||||
@@ -14,7 +14,7 @@ export interface MsIndexStatsResponse {
|
||||
fieldDistribution: Record<string, any>;
|
||||
}
|
||||
|
||||
export interface MsCreateIndex {
|
||||
export interface MsTaskStatus {
|
||||
uid: number;
|
||||
indexUid: string;
|
||||
status: string;
|
||||
@@ -48,3 +48,102 @@ export interface MsSettingsResponse<T extends {}> {
|
||||
synonyms: Record<string, string[]>;
|
||||
distinctAttribute: null | (keyof T)[] | ["*"];
|
||||
}
|
||||
|
||||
export interface MeiliSearchHealth {
|
||||
/** the current health status; is "available" when healthy */
|
||||
status: string;
|
||||
}
|
||||
|
||||
export type datetime = string;
|
||||
|
||||
export interface MeiliSearchInterface {
|
||||
uid: string;
|
||||
name: string;
|
||||
/** datetime string (aka., 2022-01-23T22:47:42.745395044Z) */
|
||||
createdAt: datetime;
|
||||
/** datetime string (aka., 2022-01-23T22:47:42.745395044Z) */
|
||||
updatedAt: datetime;
|
||||
/**
|
||||
* the property serving as the primary key for the index,
|
||||
* use `id` unless there's a good reason not to
|
||||
*/
|
||||
primaryKey: string;
|
||||
}
|
||||
|
||||
export interface MeiliSearchIndex {
|
||||
numberOfDocuments: number;
|
||||
isIndexing: false;
|
||||
fieldDistribution: Record<string, any>;
|
||||
}
|
||||
|
||||
/**
|
||||
* Global MeiliSearch Stats
|
||||
*/
|
||||
export interface MeiliSearchStats {
|
||||
databaseSize: number;
|
||||
lastUpdate: datetime;
|
||||
indexes: Record<string, MeiliSearchIndex>;
|
||||
}
|
||||
|
||||
export type GenericDoc = { id: string; _idx?: string; [key: string]: unknown };
|
||||
|
||||
export interface MeiliSearchResponse<T extends {} = GenericDoc> {
|
||||
hits: T[];
|
||||
limit: number;
|
||||
nbHits: number;
|
||||
offset: number;
|
||||
processingTimeMs: number;
|
||||
query: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* The search results but _with_ the index used inserted on
|
||||
* each "hit"
|
||||
*/
|
||||
export type WithIndex<T extends MeiliSearchResponse> = Omit<T, "hits"> & {
|
||||
hits: {
|
||||
[K in keyof T["hits"]]: T["hits"][K] & Record<K, { _idx: string }>;
|
||||
}[number];
|
||||
};
|
||||
|
||||
export interface MsIndexTasks {
|
||||
results: {
|
||||
uid: number;
|
||||
indexUid: string;
|
||||
status: string;
|
||||
type: string;
|
||||
details: {
|
||||
receiveDocuments: number;
|
||||
indexedDocuments: number;
|
||||
};
|
||||
duration: string;
|
||||
enqueuedAt: string;
|
||||
startedAt: string;
|
||||
finishedAt: string;
|
||||
}[];
|
||||
}
|
||||
|
||||
export interface MsVersion {
|
||||
comitSha: string;
|
||||
commitDate: string;
|
||||
/** semver represented as string */
|
||||
pkgVersion: string;
|
||||
}
|
||||
|
||||
export interface MsKey {
|
||||
description?: string;
|
||||
/**
|
||||
* What actions are allowed based on this key
|
||||
* ```ts
|
||||
* actions: ["documents.add", "documents.delete"]
|
||||
* ```
|
||||
*/
|
||||
actions: string[];
|
||||
/**
|
||||
* The indexes which will give the actions permissions
|
||||
*/
|
||||
indexes: string[];
|
||||
|
||||
/** todo: check that this can be a number, it CAN be a null value */
|
||||
expiresAt: number | null;
|
||||
}
|
||||
|
||||
117
src/types/model.ts
Normal file
117
src/types/model.ts
Normal file
@@ -0,0 +1,117 @@
|
||||
import { Mapper } from "~/utils/createMapper";
|
||||
import { meiliApi } from "~/utils/model-api/meiliApi";
|
||||
import { IndexSynonyms, RankingRule, RankingRulesApi } from ".";
|
||||
import { ModelMapper } from "./mapping";
|
||||
|
||||
/**
|
||||
* this represents the primary surface area for _configuring_ a search model
|
||||
*/
|
||||
export type SearchModelConfig<
|
||||
TDoc extends {},
|
||||
TMap extends MapperDictionary<string, any, TDoc> = never,
|
||||
TExclude extends string = never
|
||||
> = Omit<
|
||||
{
|
||||
searchable: (
|
||||
...props: (keyof TDoc)[]
|
||||
) => SearchModelConfig<TDoc, TMap, TExclude | "searchable">;
|
||||
displayed: (
|
||||
...props: (keyof TDoc)[]
|
||||
) => SearchModelConfig<TDoc, TMap, TExclude | "displayed">;
|
||||
distinct: (
|
||||
...props: (keyof TDoc)[]
|
||||
) => SearchModelConfig<TDoc, TMap, TExclude | "distinct">;
|
||||
filterable: (
|
||||
...props: (keyof TDoc)[]
|
||||
) => SearchModelConfig<TDoc, TMap, TExclude | "filterable">;
|
||||
sortable: (
|
||||
...props: (keyof TDoc)[]
|
||||
) => SearchModelConfig<TDoc, TMap, TExclude | "sortable">;
|
||||
/**
|
||||
* Because your website might provide content with structured English sentences, we
|
||||
* recommend adding stop words. Indeed, the search-engine would not be "spoiled" by
|
||||
* linking words and would focus on the main words of the query, rendering more
|
||||
* accurate results.
|
||||
*
|
||||
* Here is the [dedicated page about stop-words](https://docs.meilisearch.com/reference/features/stop_words.html)
|
||||
* in the official documentation. You can find more complete lists of
|
||||
* English stop-words [like this one](https://gist.github.com/sebleier/554280).
|
||||
*/
|
||||
stopWords: (words: string[]) => SearchModelConfig<TDoc, TMap, TExclude | "stopWords">;
|
||||
|
||||
// addMapper: <TInput, TOutput>(
|
||||
// m: Mapper<TInput, TOutput>
|
||||
// ) => IndexApi<T, E | typeof m["name"]>;
|
||||
|
||||
/**
|
||||
* Specify the order for the various _ranking rules_. The default ranking rules are:
|
||||
*
|
||||
* ```ts
|
||||
* ["words", "typo", "proximity", "attribute", "sort", "exactness"]
|
||||
* ```
|
||||
*
|
||||
* Refer to [Ranking Rules Documentation](https://docs.meilisearch.com/learn/core_concepts/relevancy.html#ranking-rules) for more info.
|
||||
*/
|
||||
rankingRules: (
|
||||
cb: (r: RankingRulesApi) => void
|
||||
) => SearchModelConfig<TDoc, TMap, TExclude | "rankingRules">;
|
||||
|
||||
addMapper: <N extends string>(
|
||||
name: N
|
||||
) => {
|
||||
mapDefn: <PInput>(
|
||||
mapper: ModelMapper<PInput, TDoc>
|
||||
) => SearchModelConfig<TDoc, TMap & Record<N, ModelMapper<PInput, TDoc>>, TExclude>;
|
||||
};
|
||||
},
|
||||
TExclude
|
||||
>;
|
||||
|
||||
export interface SearchModelMeta<TDoc extends {}> {
|
||||
hash?: number;
|
||||
rules?: RankingRule[];
|
||||
displayed?: (keyof TDoc)[];
|
||||
searchable?: (keyof TDoc)[];
|
||||
filterable?: (keyof TDoc)[];
|
||||
distinct?: (keyof TDoc)[];
|
||||
sortable?: (keyof TDoc)[];
|
||||
stopWords?: string[];
|
||||
synonyms?: IndexSynonyms;
|
||||
}
|
||||
|
||||
export type MapperDictionary<K extends string, I extends {}, O extends {}> = Record<
|
||||
K,
|
||||
Mapper<I, O>
|
||||
>;
|
||||
|
||||
/**
|
||||
* An encapsulation of the "state" which a model is managing.
|
||||
*/
|
||||
export type SearchModelState<
|
||||
/** document structure */
|
||||
TDoc extends {},
|
||||
/** mapper configuration */
|
||||
TMap extends MapperDictionary<string, any, TDoc> | never = never,
|
||||
TExclude extends string = never
|
||||
> = {
|
||||
/** the name of the model */
|
||||
name: Readonly<string>;
|
||||
/**
|
||||
* the TS type of the document being managed by this Model
|
||||
* ```ts
|
||||
* type Doc = typeof state.type;
|
||||
* ```
|
||||
*/
|
||||
type: TDoc;
|
||||
/**
|
||||
* The index configuration which this Model is managing
|
||||
*/
|
||||
index: SearchModelMeta<TDoc>;
|
||||
/**
|
||||
* A dictionary of mapping function which convert an expected data structure to
|
||||
* this Model's document structure.
|
||||
*/
|
||||
mappers: TMap;
|
||||
};
|
||||
|
||||
export type MeiliApi = ReturnType<typeof meiliApi>;
|
||||
124
src/utils/MeiliSearchApi.ts
Normal file
124
src/utils/MeiliSearchApi.ts
Normal file
@@ -0,0 +1,124 @@
|
||||
import { env } from "process";
|
||||
import fetch, { RequestInit } from "node-fetch";
|
||||
import {
|
||||
MeiliSearchHealth,
|
||||
MeiliSearchIndex,
|
||||
MeiliSearchResponse,
|
||||
MeiliSearchStats,
|
||||
MsVersion,
|
||||
MsAddOrReplace,
|
||||
MsIndexTasks,
|
||||
MsSettingsResponse,
|
||||
MsTaskStatus,
|
||||
MsKey,
|
||||
} from "~/types";
|
||||
import { slice } from "cheerio/lib/api/traversing";
|
||||
|
||||
export interface MeiliSearchOptions {
|
||||
url?: string;
|
||||
}
|
||||
export type PagingOptions = {
|
||||
limit?: number;
|
||||
offset?: number;
|
||||
};
|
||||
|
||||
export type ApiOptions = Omit<RequestInit, "method">;
|
||||
|
||||
export function MeiliSearchApi<TDoc extends {}>(
|
||||
idx: string,
|
||||
options: MeiliSearchOptions = {}
|
||||
) {
|
||||
const baseUrl = options.url || env.URL || "http://localhost:7700";
|
||||
|
||||
const call = async <T>(
|
||||
method: "GET" | "POST" | "PUT" | "DELETE",
|
||||
url: string,
|
||||
options: ApiOptions = {}
|
||||
): Promise<T> => {
|
||||
const res = await fetch(url, { ...options, method });
|
||||
if (res.ok) {
|
||||
return res.json() as Promise<T>;
|
||||
} else {
|
||||
throw new Error(
|
||||
`Problem calling the MeiliSearch API at the path of: ${method} ${url}. Status message was "${res.statusText}" [${res.status}]`
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
const get = <T>(url: string, options: ApiOptions = {}) => {
|
||||
return call("GET", `${baseUrl}/${url.startsWith("/" ? url.slice(1) : url)}`, options);
|
||||
};
|
||||
const put = <T>(
|
||||
url: string,
|
||||
body?: ApiOptions["body"],
|
||||
options: Omit<ApiOptions, "body"> = {}
|
||||
) => {
|
||||
return call<T>("PUT", `${baseUrl}/${url.startsWith("/" ? url.slice(1) : url)}`, {
|
||||
...options,
|
||||
body,
|
||||
});
|
||||
};
|
||||
const post = async <T>(
|
||||
url: string,
|
||||
body?: ApiOptions["body"],
|
||||
options: Omit<ApiOptions, "body"> = {}
|
||||
): Promise<T> => {
|
||||
return call<T>("POST", `${baseUrl}/${url.startsWith("/" ? url.slice(1) : url)}`, {
|
||||
...options,
|
||||
body,
|
||||
});
|
||||
};
|
||||
const del = <T>(url: string, options: ApiOptions = {}) => {
|
||||
return call<T>(
|
||||
"DELETE",
|
||||
`${baseUrl}/${url.startsWith("/" ? url.slice(1) : url)}`,
|
||||
options
|
||||
);
|
||||
};
|
||||
|
||||
const endpoints = {
|
||||
// per index
|
||||
getIndexTasks: get<MsIndexTasks>(`indexes/${idx}/tasks`),
|
||||
getDocument: (docId: string) => get<TDoc>(`indexes/${idx}/documents/${docId}`),
|
||||
deleteDocument: (docId: string) =>
|
||||
del<MsTaskStatus>(`indexes/${idx}/documents/${docId}`),
|
||||
getDocuments: (paging: PagingOptions = {}) => get<TDoc[]>(`indexes/${idx}/documents`),
|
||||
deleteAllDocuments: del<MsTaskStatus>(`indexes/${idx}/documents`),
|
||||
addOrReplaceDocuments: (doc: TDoc, o: ApiOptions = {}) =>
|
||||
post<MsAddOrReplace>(`indexes/${idx}/documents`, JSON.stringify(doc), o),
|
||||
addOrUpdateDocuments: (doc: TDoc, o: ApiOptions = {}) =>
|
||||
put<MsAddOrReplace>(`indexes/${idx}/documents`, JSON.stringify(doc), o),
|
||||
search: (text: string) => get<MeiliSearchResponse>(`indexes/${idx}/search?q=${text}`),
|
||||
getAllIndexSettings: get<MsSettingsResponse<TDoc>>(`indexes/${idx}/settings`),
|
||||
updateIndexSettings: (settings: MsSettingsResponse<TDoc>) =>
|
||||
post<MsTaskStatus>(`indexes/${idx}/settings`, JSON.stringify(settings)),
|
||||
resetIndexSettings: del<MsTaskStatus>(`indexes/${idx}/settings`),
|
||||
updateRankingRules: post<MsTaskStatus>(`indexes/${idx}/settings/ranking-rules`),
|
||||
updateDistinctAttribute: post<MsTaskStatus>(
|
||||
`indexes/${idx}/settings/distinct-attribute`
|
||||
),
|
||||
updateSearchableAttributes: post<MsTaskStatus>(
|
||||
`indexes/${idx}/settings/searchable-attributes`
|
||||
),
|
||||
updateSortableAttributes: post<MsTaskStatus>(
|
||||
`indexes/${idx}/settings/sortable-attributes`
|
||||
),
|
||||
updateDisplayedAttributes: post<MsTaskStatus>(
|
||||
`indexes/${idx}/settings/displayed-attributes`
|
||||
),
|
||||
updateSynonyms: post<MsTaskStatus>(`indexes/${idx}/settings/synonyms`),
|
||||
updateStopWords: post<MsTaskStatus>(`indexes/${idx}/settings/stop-words`),
|
||||
|
||||
// cross-index
|
||||
|
||||
stats: get<MeiliSearchStats>(`stats`),
|
||||
health: get<MeiliSearchHealth>(`health`),
|
||||
indexes: get<MeiliSearchIndex>(`indexes`),
|
||||
version: get<MsVersion>(`version`),
|
||||
getKeys: get<MsKey[]>(`keys`),
|
||||
createKey: (key: MsKey) => post<MsTaskStatus>(`keys`, JSON.stringify(key)),
|
||||
deleteKey: (key: string) => del<MsTaskStatus>(`keys/${key}`),
|
||||
};
|
||||
|
||||
return { get, put, post, delete: del, endpoints };
|
||||
}
|
||||
@@ -1,207 +1,37 @@
|
||||
import {
|
||||
IndexSynonyms,
|
||||
MsAddOrReplace,
|
||||
MsCreateIndex,
|
||||
MsIndexStatsResponse,
|
||||
MsIndexStatusResponse,
|
||||
MsSearchResponse,
|
||||
MsSettingsResponse,
|
||||
RankingRule,
|
||||
RankingRulesApi,
|
||||
MapperDictionary,
|
||||
SearchModelState,
|
||||
SearchModel,
|
||||
SearchModelConfig,
|
||||
} from "~/types";
|
||||
import xxhash from "xxhash-wasm";
|
||||
import fetch from "node-fetch";
|
||||
import { getUrl } from "./getUrl";
|
||||
import { rankingRules } from "./model-api/rankingRules";
|
||||
|
||||
export type IndexApi<T, E extends string = never> = Omit<
|
||||
{
|
||||
searchable: (...props: (keyof T)[]) => IndexApi<T, E | "searchable">;
|
||||
displayed: (...props: (keyof T)[]) => IndexApi<T, E | "displayed">;
|
||||
distinct: (...props: (keyof T)[]) => IndexApi<T, E | "distinct">;
|
||||
filterable: (...props: (keyof T)[]) => IndexApi<T, E | "filterable">;
|
||||
sortable: (...props: (keyof T)[]) => IndexApi<T, E | "sortable">;
|
||||
/**
|
||||
* Because your website might provide content with structured English sentences, we
|
||||
* recommend adding stop words. Indeed, the search-engine would not be "spoiled" by
|
||||
* linking words and would focus on the main words of the query, rendering more
|
||||
* accurate results.
|
||||
*
|
||||
* Here is the [dedicated page about stop-words](https://docs.meilisearch.com/reference/features/stop_words.html)
|
||||
* in the official documentation. You can find more complete lists of
|
||||
* English stop-words [like this one](https://gist.github.com/sebleier/554280).
|
||||
*/
|
||||
stopWords: (words: string[]) => IndexApi<T, E | "stopWords">;
|
||||
import { meiliApi } from "./model-api/meiliApi";
|
||||
import { mappingApi } from "./model-api/mappingApi";
|
||||
import { modelConfigApi } from "./model-api/modelConfig";
|
||||
|
||||
// addMapper: <TInput, TOutput>(
|
||||
// m: Mapper<TInput, TOutput>
|
||||
// ) => IndexApi<T, E | typeof m["name"]>;
|
||||
|
||||
/**
|
||||
* Specify the order for the various _ranking rules_. The default ranking rules are:
|
||||
*
|
||||
* ```ts
|
||||
* ["words", "typo", "proximity", "attribute", "sort", "exactness"]
|
||||
* ```
|
||||
*
|
||||
* Refer to [Ranking Rules Documentation](https://docs.meilisearch.com/learn/core_concepts/relevancy.html#ranking-rules) for more info.
|
||||
*/
|
||||
rankingRules: (cb: (r: RankingRulesApi) => void) => IndexApi<T, E | "rankingRules">;
|
||||
},
|
||||
E
|
||||
>;
|
||||
|
||||
const modelConfigApi = <T extends {}>(update: (s: PartialModel<T>) => void) => {
|
||||
const api = <E extends string = never, M extends string = never>(): IndexApi<T, E> =>
|
||||
({
|
||||
searchable(...props) {
|
||||
if (props?.length > 0) {
|
||||
update({ info: { searchable: props } });
|
||||
}
|
||||
return api<E | "searchable", M>();
|
||||
},
|
||||
displayed(...props) {
|
||||
if (props?.length > 0) {
|
||||
update({ info: { displayed: props } });
|
||||
}
|
||||
return api<E | "displayed", M>();
|
||||
},
|
||||
distinct(...props) {
|
||||
if (props?.length > 0) {
|
||||
update({ info: { distinct: props } });
|
||||
}
|
||||
return api<E | "distinct", M>();
|
||||
},
|
||||
filterable(...props) {
|
||||
if (props?.length > 0) {
|
||||
update({ info: { filterable: props } });
|
||||
}
|
||||
return api<E | "filterable", M>();
|
||||
},
|
||||
sortable(...props) {
|
||||
if (props?.length > 0) {
|
||||
update({ info: { sortable: props } });
|
||||
}
|
||||
return api<E | "searchable", M>();
|
||||
},
|
||||
stopWords(words) {
|
||||
update({ info: { stopWords: words } });
|
||||
return api<E | "stopWords">();
|
||||
},
|
||||
|
||||
rankingRules(cb: (r: RankingRulesApi) => void) {
|
||||
const updateRules = (r: RankingRule[]) => {
|
||||
update({ info: { rules: r } });
|
||||
};
|
||||
const ruleApi = rankingRules(updateRules);
|
||||
cb(ruleApi);
|
||||
|
||||
return api<E | "rankingRules">();
|
||||
},
|
||||
|
||||
// addMapper<TInput extends {}, TOutput extends {}>(
|
||||
// mapper: Mapper<TInput, TOutput>
|
||||
// ) {
|
||||
// update({ mappers: })
|
||||
// return api<E | typeof mapper["name"]>
|
||||
// },
|
||||
} as IndexApi<T, E>);
|
||||
|
||||
return api();
|
||||
};
|
||||
|
||||
export type SearchModel<T extends {}> = {
|
||||
name: string;
|
||||
hash: number;
|
||||
info: {
|
||||
type: T;
|
||||
rules?: RankingRule[];
|
||||
displayed?: (keyof T)[];
|
||||
searchable?: (keyof T)[];
|
||||
filterable?: (keyof T)[];
|
||||
distinct?: (keyof T)[];
|
||||
sortable?: (keyof T)[];
|
||||
stopWords?: string[];
|
||||
synonyms?: IndexSynonyms;
|
||||
const finalize =
|
||||
(name: string, initial: ) =>
|
||||
<TDoc extends {}, TMap extends MapperDictionary<string, any, TDoc>>(
|
||||
state: SearchModelState<TDoc, TMap>
|
||||
) => {
|
||||
return {
|
||||
...state,
|
||||
api: meiliApi<TDoc>(state.name),
|
||||
...mappingApi<TDoc, TMap>(name, state.mappers),
|
||||
} as SearchModel<TDoc, TMap>;
|
||||
};
|
||||
query: {
|
||||
createIndex(): Promise<MsCreateIndex>;
|
||||
showIndex(): Promise<MsIndexStatusResponse>;
|
||||
showIndexStats(): Promise<MsIndexStatsResponse>;
|
||||
addOrReplaceDoc(doc: T): Promise<MsAddOrReplace>;
|
||||
search(find: string): Promise<MsSearchResponse<T>>;
|
||||
getAllSettings(): Promise<MsSettingsResponse<T>>;
|
||||
};
|
||||
toString(): string;
|
||||
};
|
||||
|
||||
export type PartialModel<T extends {}> = Omit<Partial<SearchModel<T>>, "info"> & {
|
||||
info: Partial<SearchModel<T>["info"]>;
|
||||
};
|
||||
|
||||
export const createModel = async <T extends Record<string, any>>(
|
||||
export const createModel = <TDoc extends {}>(
|
||||
name: string,
|
||||
cb?: (api: IndexApi<T>) => void
|
||||
cb?: (api: SearchModelConfig<TDoc, never, never>) => void
|
||||
) => {
|
||||
const { h32 } = await xxhash();
|
||||
|
||||
let state: SearchModel<T> = {
|
||||
name,
|
||||
hash: h32("name"),
|
||||
info: {
|
||||
type: {} as unknown as T,
|
||||
},
|
||||
query: {
|
||||
createIndex: async () => {
|
||||
return (await (
|
||||
await fetch(getUrl(`indexes/${name}`), {
|
||||
method: "POST",
|
||||
body: JSON.stringify({ uid: name, primaryKey: "id" }),
|
||||
})
|
||||
).json()) as MsCreateIndex;
|
||||
},
|
||||
showIndex: async () => {
|
||||
return (await (
|
||||
await fetch(getUrl(`indexes/${name}`))
|
||||
).json()) as MsIndexStatusResponse;
|
||||
},
|
||||
showIndexStats: async () => {
|
||||
return (await (
|
||||
await fetch(getUrl(`indexes/${name}/stats`))
|
||||
).json()) as MsIndexStatsResponse;
|
||||
},
|
||||
addOrReplaceDoc: async (doc: T) => {
|
||||
return (await (
|
||||
await fetch(getUrl(`indexes/${name}/documents`), {
|
||||
method: "POST",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
body: JSON.stringify(doc),
|
||||
})
|
||||
).json()) as MsAddOrReplace;
|
||||
},
|
||||
getAllSettings: async () => {
|
||||
return (await (
|
||||
await fetch(getUrl(`indexes/${name}`))
|
||||
).json()) as MsSettingsResponse<T>;
|
||||
},
|
||||
search: async (find: string) => {
|
||||
return (await (
|
||||
await fetch(getUrl(`indexes/${name}/search?q=${find}`))
|
||||
).json()) as MsSearchResponse<T>;
|
||||
},
|
||||
},
|
||||
};
|
||||
const updateState = (s: PartialModel<T>) => {
|
||||
if (s.info) {
|
||||
state.info = { ...state.info, ...s.info };
|
||||
state.hash = h32(JSON.stringify({ ...state.info, name: state.name }));
|
||||
}
|
||||
};
|
||||
const f = finalize(name, { state: { name } });
|
||||
|
||||
if (cb) {
|
||||
cb(modelConfigApi<T>(updateState));
|
||||
const api = modelConfigApi<TDoc>(name, f);
|
||||
cb(api);
|
||||
} else {
|
||||
// default config
|
||||
}
|
||||
|
||||
return {
|
||||
@@ -209,5 +39,5 @@ export const createModel = async <T extends Record<string, any>>(
|
||||
toString() {
|
||||
return `Model(${name}::${state.hash})`;
|
||||
},
|
||||
} as SearchModel<T>;
|
||||
} as SearchModel<TDoc, TMap>;
|
||||
};
|
||||
|
||||
@@ -1,13 +0,0 @@
|
||||
import { env } from "process";
|
||||
|
||||
export function getUrl(offset: string = "") {
|
||||
const host = env.HOST || "localhost";
|
||||
const port = env.PORT || "7700";
|
||||
const protocol =
|
||||
env.PROTO || host === "localhost" || host.startsWith("192.") ? "http://" : "https://";
|
||||
const url = env.URL ? env.URL : `${protocol}${host}:${port}/`;
|
||||
if (offset.startsWith("/")) {
|
||||
offset = offset.slice(1);
|
||||
}
|
||||
return url.endsWith("/") ? `${url}${offset}` : `${url}/${offset}`;
|
||||
}
|
||||
25
src/utils/model-api/mappingApi.ts
Normal file
25
src/utils/model-api/mappingApi.ts
Normal file
@@ -0,0 +1,25 @@
|
||||
import { MapperDictionary, MsAddOrReplace } from "~/types";
|
||||
import { MeiliSearchApi } from "../MeiliSearchApi";
|
||||
|
||||
export function mappingApi<
|
||||
TDoc extends {},
|
||||
TMap extends MapperDictionary<string, any, TDoc>
|
||||
>(idx: string, mappings: TMap) {
|
||||
return {
|
||||
mapWith: <K extends keyof TMap>(
|
||||
map: K,
|
||||
data: Parameters<TMap[K]["map"]>[0]
|
||||
): TDoc => {
|
||||
return mappings[map].map(data) as TDoc;
|
||||
},
|
||||
|
||||
updateWith: async <K extends keyof TMap, D extends Parameters<TMap[K]["map"]>[0]>(
|
||||
map: K,
|
||||
data: D
|
||||
): Promise<MsAddOrReplace> => {
|
||||
const { endpoints } = MeiliSearchApi(idx);
|
||||
const doc = mappings[map].map(data);
|
||||
return endpoints.addOrReplaceDocuments(doc);
|
||||
},
|
||||
};
|
||||
}
|
||||
7
src/utils/model-api/meiliApi.ts
Normal file
7
src/utils/model-api/meiliApi.ts
Normal file
@@ -0,0 +1,7 @@
|
||||
import { MeiliSearchApi } from "../MeiliSearchApi";
|
||||
|
||||
/** adds implementation for the API endpoints of MeiliSearch */
|
||||
export function meiliApi<TDoc extends {}>(idx: string) {
|
||||
const { endpoints } = MeiliSearchApi(idx);
|
||||
return endpoints;
|
||||
}
|
||||
126
src/utils/model-api/modelConfig.ts
Normal file
126
src/utils/model-api/modelConfig.ts
Normal file
@@ -0,0 +1,126 @@
|
||||
import {
|
||||
MapperDictionary,
|
||||
ModelMapper,
|
||||
RankingRule,
|
||||
RankingRulesApi,
|
||||
SearchModelConfig,
|
||||
SearchModelState,
|
||||
} from "~/types";
|
||||
import { rankingRules } from "./rankingRules";
|
||||
|
||||
/**
|
||||
* Higher order fn for managing configuration of a Model.
|
||||
*
|
||||
* In this first call you just pass in the `TDoc` generic
|
||||
* along with the model name. This will return a Tuple
|
||||
* with the config API and a function to call for results.
|
||||
*/
|
||||
export const modelConfigApi = <TDoc extends {}>(
|
||||
/** the name of the index */
|
||||
name: string,
|
||||
/**
|
||||
* A callback function which allows the configurator to call during configuration
|
||||
* process so that `createModel()` can pull out the final configuration.
|
||||
*/
|
||||
finalize: <F extends SearchModelState<TDoc, MapperDictionary<string, {}, TDoc>>>(
|
||||
state: F
|
||||
) => void
|
||||
) => {
|
||||
const api = <
|
||||
TMap extends MapperDictionary<string, any, TDoc> = never,
|
||||
TExclude extends string = never
|
||||
>(
|
||||
state: SearchModelState<TDoc, TMap, TExclude>
|
||||
): SearchModelConfig<TDoc, TMap, TExclude> =>
|
||||
({
|
||||
searchable(...props: (keyof TDoc)[]) {
|
||||
return api<TMap, TExclude | "searchable">({
|
||||
...state,
|
||||
index: {
|
||||
...state.index,
|
||||
searchable: props,
|
||||
},
|
||||
});
|
||||
},
|
||||
displayed(...props: (keyof TDoc)[]) {
|
||||
return api<TMap, TExclude | "displayed">({
|
||||
...state,
|
||||
index: {
|
||||
...state.index,
|
||||
displayed: props,
|
||||
},
|
||||
});
|
||||
},
|
||||
distinct(...props: (keyof TDoc)[]) {
|
||||
return api<TMap, TExclude | "distinct">({
|
||||
...state,
|
||||
index: {
|
||||
...state.index,
|
||||
distinct: props,
|
||||
},
|
||||
});
|
||||
},
|
||||
filterable(...props: (keyof TDoc)[]) {
|
||||
return api<TMap, TExclude | "filterable">({
|
||||
...state,
|
||||
index: {
|
||||
...state.index,
|
||||
filterable: props,
|
||||
},
|
||||
});
|
||||
},
|
||||
sortable(...props: (keyof TDoc)[]) {
|
||||
return api<TMap, TExclude | "sortable">({
|
||||
...state,
|
||||
index: {
|
||||
...state.index,
|
||||
sortable: props,
|
||||
},
|
||||
});
|
||||
},
|
||||
stopWords(words: string[]) {
|
||||
return api<TMap, TExclude | "sortable">({
|
||||
...state,
|
||||
index: {
|
||||
...state.index,
|
||||
stopWords: words,
|
||||
},
|
||||
});
|
||||
},
|
||||
|
||||
rankingRules(cb: (r: RankingRulesApi) => void) {
|
||||
let r: RankingRule[] = [];
|
||||
const update = (rules: RankingRule[]) => {
|
||||
r = rules;
|
||||
};
|
||||
const ruleApi = rankingRules(update);
|
||||
cb(ruleApi);
|
||||
|
||||
return api<TMap, TExclude | "sortable">({
|
||||
...state,
|
||||
index: {
|
||||
...state.index,
|
||||
rank: r,
|
||||
},
|
||||
});
|
||||
},
|
||||
|
||||
addMapper<N extends string>(mapName: N) {
|
||||
return {
|
||||
mapDefn: <TInput extends {}>(mapper: ModelMapper<TInput, TDoc>) => {
|
||||
return api<TMap & Record<N, ModelMapper<TInput, TDoc>>, TExclude>({
|
||||
...state,
|
||||
mappers: { ...state.mappers, [mapName]: mapper },
|
||||
});
|
||||
},
|
||||
};
|
||||
},
|
||||
} as unknown as SearchModelConfig<TDoc, TMap, TExclude>);
|
||||
|
||||
const response = {
|
||||
finalize,
|
||||
api,
|
||||
};
|
||||
|
||||
return api();
|
||||
};
|
||||
@@ -1,31 +1,52 @@
|
||||
import { createModel } from "~/utils/createModel";
|
||||
|
||||
type Model = {
|
||||
title: string;
|
||||
section: string;
|
||||
lastUpdated: number;
|
||||
};
|
||||
|
||||
type AstModel = {
|
||||
h1: string;
|
||||
h2: string;
|
||||
lastUpdated: string;
|
||||
};
|
||||
|
||||
describe("createModel()", () => {
|
||||
it("Configuring index properties on a model is seen in model's info section", async () => {
|
||||
const m = await createModel<Model>("foobar", (m) =>
|
||||
const m = createModel<Model>("foobar", (m) =>
|
||||
m //
|
||||
.sortable("section")
|
||||
.displayed("title", "section")
|
||||
.filterable("lastUpdated")
|
||||
);
|
||||
|
||||
expect(m.info.displayed).toContain("title");
|
||||
expect(m.info.displayed).toContain("section");
|
||||
expect(m.info.displayed).not.toContain("lastUpdated");
|
||||
expect(m.meta.displayed).toContain("title");
|
||||
expect(m.meta.displayed).toContain("section");
|
||||
expect(m.meta.displayed).not.toContain("lastUpdated");
|
||||
|
||||
expect(m.info.sortable).toContain("section");
|
||||
expect(m.info.filterable).toContain("lastUpdated");
|
||||
expect(m.meta.sortable).toContain("section");
|
||||
expect(m.meta.filterable).toContain("lastUpdated");
|
||||
});
|
||||
|
||||
it("toString() produces a clear and concise indication of what model it is", async () => {
|
||||
const m = await createModel<Model>("foobar");
|
||||
const m = createModel<Model>("foobar");
|
||||
expect(m.toString()).toContain("Model");
|
||||
expect(m.toString()).toContain("foobar::");
|
||||
expect(m.toString()).toContain(String(m.hash));
|
||||
expect(m.toString()).toContain(String(m.meta.hash));
|
||||
});
|
||||
|
||||
it("adding mappers to a model, exposes this on the model API", () => {
|
||||
// define a mapper on `m`
|
||||
const m = createModel<Model>("foobar", (c) =>
|
||||
c //
|
||||
.addMapper("doc")
|
||||
.mapDefn<AstModel>((i) => ({
|
||||
title: i.h1,
|
||||
section: i.h2,
|
||||
lastUpdated: new Date(i.lastUpdated).getTime(),
|
||||
}))
|
||||
.searchable("title", "section")
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -2,11 +2,12 @@ import { readFileSync } from "fs";
|
||||
import matter from "gray-matter";
|
||||
|
||||
describe("frontmatter tests", () => {
|
||||
|
||||
it("faq", () => {
|
||||
const content = readFileSync("test/fixtures/prose/faq.md", "utf-8");
|
||||
const content = readFileSync(
|
||||
"test/fixtures/prose/guides/contributor-guide.md",
|
||||
"utf-8"
|
||||
);
|
||||
const output = matter(content);
|
||||
console.log(output)
|
||||
console.log(output);
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user