mirror of
				https://github.com/falsycat/ar.falsy.cat.git
				synced 2025-10-30 21:28:20 +00:00 
			
		
		
		
	feat: add support for semantic search using operand
This commit is contained in:
		
							
								
								
									
										1
									
								
								.gitignore
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										1
									
								
								.gitignore
									
									
									
									
										vendored
									
									
								
							| @@ -5,3 +5,4 @@ resources | |||||||
| content/.obsidian | content/.obsidian | ||||||
| assets/indices/linkIndex.json | assets/indices/linkIndex.json | ||||||
| assets/indices/contentIndex.json | assets/indices/contentIndex.json | ||||||
|  | linkmap | ||||||
|   | |||||||
| @@ -56,6 +56,6 @@ | |||||||
|     } |     } | ||||||
|     const allIds = new Set([...getByField("title"), ...getByField("content")]) |     const allIds = new Set([...getByField("title"), ...getByField("content")]) | ||||||
|     const finalResults = [...allIds].map(formatForDisplay) |     const finalResults = [...allIds].map(formatForDisplay) | ||||||
|     displayResults(finalResults) |     displayResults(finalResults, true) | ||||||
|   }) |   }) | ||||||
| })() | })() | ||||||
							
								
								
									
										35
									
								
								assets/js/semantic-search.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										35
									
								
								assets/js/semantic-search.js
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,35 @@ | |||||||
|  | const apiKey = "{{$.Site.Data.config.operandApiKey}}" | ||||||
|  |  | ||||||
|  | async function searchContents(query) { | ||||||
|  |   const response = await fetch('https://prod.operand.ai/v3/search/objects', { | ||||||
|  |     method: 'POST', | ||||||
|  |     headers: { | ||||||
|  |       'Content-Type': 'application/json', | ||||||
|  |       Authorization: apiKey, | ||||||
|  |     }, | ||||||
|  |     body: JSON.stringify({ | ||||||
|  |       query, | ||||||
|  |       max: 10 | ||||||
|  |     }), | ||||||
|  |   }); | ||||||
|  |   return (await response.json()); | ||||||
|  | } | ||||||
|  |  | ||||||
|  | function debounce(func, timeout = 300) { | ||||||
|  |   let timer; | ||||||
|  |   return (...args) => { | ||||||
|  |     clearTimeout(timer) | ||||||
|  |     timer = setTimeout(() => { func.apply(this, args); }, timeout) | ||||||
|  |   }; | ||||||
|  | } | ||||||
|  |  | ||||||
|  | registerHandlers(debounce((e) => { | ||||||
|  |   term = e.target.value | ||||||
|  |   searchContents(term) | ||||||
|  |     .then((res) => res.results.map(entry => ({ | ||||||
|  |       url: entry.object.metadata.url, | ||||||
|  |       content: entry.snippet, | ||||||
|  |       title: entry.object.title | ||||||
|  |     }))) | ||||||
|  |     .then(results => displayResults(results)) | ||||||
|  | })) | ||||||
| @@ -108,13 +108,11 @@ const highlight = (content, term) => { | |||||||
| } | } | ||||||
|  |  | ||||||
| // Common utilities for search | // Common utilities for search | ||||||
| const resultToHTML = ({ url, title, content, term }) => { | const resultToHTML = ({ url, title, content }) => { | ||||||
|   const text = removeMarkdown(content) |   const cleaned = removeMarkdown(content) | ||||||
|   const resultTitle = highlight(title, term) |  | ||||||
|   const resultText = highlight(text, term) |  | ||||||
|   return `<button class="result-card" id="${url}"> |   return `<button class="result-card" id="${url}"> | ||||||
|       <h3>${resultTitle}</h3> |       <h3>${title}</h3> | ||||||
|       <p>${resultText}</p> |       <p>${cleaned}</p> | ||||||
|   </button>` |   </button>` | ||||||
| } | } | ||||||
|  |  | ||||||
| @@ -183,7 +181,7 @@ const registerHandlers = (onInputFn) => { | |||||||
|   }) |   }) | ||||||
| } | } | ||||||
|  |  | ||||||
| const displayResults = (finalResults) => { | const displayResults = (finalResults, extractHighlight = false) => { | ||||||
|   const results = document.getElementById("results-container") |   const results = document.getElementById("results-container") | ||||||
|   if (finalResults.length === 0) { |   if (finalResults.length === 0) { | ||||||
|     results.innerHTML = `<button class="result-card"> |     results.innerHTML = `<button class="result-card"> | ||||||
| @@ -192,11 +190,17 @@ const displayResults = (finalResults) => { | |||||||
|                 </button>` |                 </button>` | ||||||
|   } else { |   } else { | ||||||
|     results.innerHTML = finalResults |     results.innerHTML = finalResults | ||||||
|       .map((result) => |       .map((result) => { | ||||||
|         resultToHTML({ |           if (extractHighlight) { | ||||||
|           ...result, |             return resultToHTML({ | ||||||
|           term, |               url: result.url, | ||||||
|         }), |               title: highlight(result.title, term), | ||||||
|  |               content: highlight(result.content, term) | ||||||
|  |             }) | ||||||
|  |           } else { | ||||||
|  |             return resultToHTML(result) | ||||||
|  |           } | ||||||
|  |         } | ||||||
|       ) |       ) | ||||||
|       .join("\n") |       .join("\n") | ||||||
|     const anchors = [...document.getElementsByClassName("result-card")] |     const anchors = [...document.getElementsByClassName("result-card")] | ||||||
|   | |||||||
| @@ -54,9 +54,13 @@ enableRecentNotes: false | |||||||
|  |  | ||||||
| # whether to display and 'edit' button next to the last edited field | # whether to display and 'edit' button next to the last edited field | ||||||
| # that links to github | # that links to github | ||||||
| enableGitHubEdit: false | enableGitHubEdit: true | ||||||
| GitHubLink: https://github.com/jackyzha0/quartz/tree/hugo/content | GitHubLink: https://github.com/jackyzha0/quartz/tree/hugo/content | ||||||
|  |  | ||||||
|  | # whether to use Operand to power semantic search | ||||||
|  | enableSemanticSearch: true | ||||||
|  | operandApiKey: "1e47d93b-1468-45b7-98d5-7f733d5e45e2" | ||||||
|  |  | ||||||
| # page description used for SEO | # page description used for SEO | ||||||
| description: | description: | ||||||
|   Host your second brain and digital garden for free. Quartz features extremely fast full-text search, |   Host your second brain and digital garden for free. Quartz features extremely fast full-text search, | ||||||
|   | |||||||
| @@ -10,8 +10,10 @@ enableSPA: true | |||||||
| enableFooter: true | enableFooter: true | ||||||
| enableContextualBacklinks: true | enableContextualBacklinks: true | ||||||
| enableRecentNotes: false | enableRecentNotes: false | ||||||
| enableGitHubEdit: false | enableGitHubEdit: true | ||||||
| GitHubLink: https://github.com/jackyzha0/quartz/tree/hugo/content | GitHubLink: https://github.com/jackyzha0/quartz/tree/hugo/content | ||||||
|  | enableSemanticSearch: true | ||||||
|  | operandApiKey: "1e47d93b-1468-45b7-98d5-7f733d5e45e2" | ||||||
| description: | description: | ||||||
|   Host your second brain and digital garden for free. Quartz features extremely fast full-text search, |   Host your second brain and digital garden for free. Quartz features extremely fast full-text search, | ||||||
|   Wikilink support, backlinks, local graph, tags, and link previews. |   Wikilink support, backlinks, local graph, tags, and link previews. | ||||||
|   | |||||||
| @@ -1,3 +1,3 @@ | |||||||
| {{if $.Site.Data.config.enableGitHubEdit}} | {{if $.Site.Data.config.enableGitHubEdit}} | ||||||
| <a href="{{$.Site.Data.config.GitHubLink}}/{{.Path}}" rel="noopener">Edit Source</a> | <a href="{{$.Site.Data.config.GitHubLink}}/{{.File.Path}}" rel="noopener">Edit Source</a> | ||||||
| {{end}} | {{end}} | ||||||
|   | |||||||
| @@ -6,7 +6,13 @@ | |||||||
|     </div> |     </div> | ||||||
|   </div> |   </div> | ||||||
| </div> | </div> | ||||||
|  | {{if $.Site.Data.config.enableSemanticSearch}} | ||||||
|  | {{ $js := resources.Get "js/semantic-search.js" | resources.ExecuteAsTemplate "js/semantic-search.js" . | resources.Fingerprint "md5" | resources.Minify }} | ||||||
|  | <script defer src="{{ $js.Permalink }}"></script> | ||||||
|  | {{else}} | ||||||
| <script src="https://cdn.jsdelivr.net/npm/flexsearch@0.7.21/dist/flexsearch.bundle.js" | <script src="https://cdn.jsdelivr.net/npm/flexsearch@0.7.21/dist/flexsearch.bundle.js" | ||||||
|   integrity="sha256-i3A0NZGkhsKjVMzFxv3ksk0DZh3aXqu0l49Bbh0MdjE=" crossorigin="anonymous" defer></script> |   integrity="sha256-i3A0NZGkhsKjVMzFxv3ksk0DZh3aXqu0l49Bbh0MdjE=" crossorigin="anonymous" defer></script> | ||||||
| {{ $js := resources.Get "js/search.js" | resources.Fingerprint "md5" | resources.Minify }} | {{ $js := resources.Get "js/full-text-search.js" | resources.Fingerprint "md5" | resources.Minify }} | ||||||
| <script defer src="{{ $js.Permalink }}"></script> | <script defer src="{{ $js.Permalink }}"></script> | ||||||
|  | {{end}} | ||||||
|  |  | ||||||
|   | |||||||
		Reference in New Issue
	
	Block a user