152 lines
		
	
	
		
			4.6 KiB
		
	
	
	
		
			JavaScript
		
	
	
	
	
	
			
		
		
	
	
			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;
 | |
| }
 |