ObsiViewer/server/search.mapping.mjs

152 lines
4.6 KiB
JavaScript

/**
* Map Obsidian-style search queries to Meilisearch format
* Supports: tag:, path:, file: operators with free text search
*/
export function mapObsidianQueryToMeili(qRaw) {
const tokens = String(qRaw).trim().split(/\s+/);
const filters = [];
const meiliQ = [];
let restrict = null;
let rangeStart = null;
let rangeEnd = null;
const parseDate = (s) => {
if (!s) return null;
// Accept YYYY-MM-DD
const m = String(s).match(/^(\d{4})-(\d{2})-(\d{2})$/);
if (!m) return null;
const d = new Date(`${m[1]}-${m[2]}-${m[3]}T00:00:00Z`);
if (Number.isNaN(d.getTime())) return null;
return d;
};
for (const token of tokens) {
// tag: operator - filter by exact tag match
if (token.startsWith('tag:')) {
const value = token.slice(4).replace(/^["']|["']$/g, '');
if (value) {
// Remove leading # if present
const cleanTag = value.startsWith('#') ? value.substring(1) : value;
filters.push(`tags = "${cleanTag}"`);
}
}
// path: operator - filter by parent directory path
// We use parentDirs array to simulate startsWith behavior
else if (token.startsWith('path:')) {
const value = token.slice(5).replace(/^["']|["']$/g, '');
if (value) {
// Normalize path separators
const normalizedPath = value.replace(/\\/g, '/');
filters.push(`parentDirs = "${normalizedPath}"`);
}
}
// file: operator - restrict search to file field
else if (token.startsWith('file:')) {
const value = token.slice(5).replace(/^["']|["']$/g, '');
if (value) {
// Restrict search to file field only
restrict = ['file'];
meiliQ.push(value);
}
}
// year: operator - facet filter
else if (token.startsWith('year:')) {
const value = token.slice(5).replace(/^["']|["']$/g, '');
const n = Number(value);
if (!Number.isNaN(n)) {
filters.push(`year = ${n}`);
}
}
// month: operator - facet filter (1-12)
else if (token.startsWith('month:')) {
const value = token.slice(6).replace(/^["']|["']$/g, '');
const n = Number(value);
if (!Number.isNaN(n)) {
filters.push(`month = ${n}`);
}
}
// date:YYYY-MM-DD (single day)
else if (token.startsWith('date:')) {
const value = token.slice(5);
const d = parseDate(value);
if (d) {
const start = new Date(d); start.setUTCHours(0,0,0,0);
const end = new Date(d); end.setUTCHours(23,59,59,999);
const startMs = start.getTime();
const endMs = end.getTime();
filters.push(`((createdAt >= ${startMs} AND createdAt <= ${endMs}) OR (updatedAt >= ${startMs} AND updatedAt <= ${endMs}))`);
}
}
// from:/to: YYYY-MM-DD
else if (token.startsWith('from:')) {
const d = parseDate(token.slice(5));
if (d) {
const s = new Date(d); s.setUTCHours(0,0,0,0);
rangeStart = s.getTime();
}
}
else if (token.startsWith('to:')) {
const d = parseDate(token.slice(3));
if (d) {
const e = new Date(d); e.setUTCHours(23,59,59,999);
rangeEnd = e.getTime();
}
}
// Regular text token
else if (token) {
meiliQ.push(token);
}
}
// If we captured a from/to range, add a combined date filter
if (rangeStart !== null || rangeEnd !== null) {
const startMs = rangeStart ?? 0;
const endMs = rangeEnd ?? 8640000000000000; // large future
filters.push(`((createdAt >= ${startMs} AND createdAt <= ${endMs}) OR (updatedAt >= ${startMs} AND updatedAt <= ${endMs}))`);
}
return {
meiliQ: meiliQ.join(' ').trim(),
filters,
restrict
};
}
/**
* Build Meilisearch search parameters from parsed query
*/
export function buildSearchParams(parsedQuery, options = {}) {
const {
limit = 20,
offset = 0,
sort,
highlight = true
} = options;
const params = {
q: parsedQuery.meiliQ,
limit: Number(limit),
offset: Number(offset),
filter: parsedQuery.filters.length ? parsedQuery.filters.join(' AND ') : undefined,
facets: ['tags', 'parentDirs', 'year', 'month'],
attributesToHighlight: highlight ? ['title', 'content'] : [],
highlightPreTag: '<mark>',
highlightPostTag: '</mark>',
attributesToCrop: ['content'],
cropLength: 80,
cropMarker: '…',
attributesToSearchOn: parsedQuery.restrict?.length ? parsedQuery.restrict : undefined,
sort: sort ? [String(sort)] : undefined,
showMatchesPosition: false
};
// Remove undefined values
Object.keys(params).forEach(key => {
if (params[key] === undefined) {
delete params[key];
}
});
return params;
}