prompt_manager 0.5.7 → 0.5.8
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/CHANGELOG.md +4 -0
- data/COMMITS.md +196 -0
- data/README.md +485 -203
- data/docs/.keep +0 -0
- data/docs/advanced/custom-keywords.md +421 -0
- data/docs/advanced/dynamic-directives.md +535 -0
- data/docs/advanced/performance.md +612 -0
- data/docs/advanced/search-integration.md +635 -0
- data/docs/api/configuration.md +355 -0
- data/docs/api/directive-processor.md +431 -0
- data/docs/api/prompt-class.md +354 -0
- data/docs/api/storage-adapters.md +462 -0
- data/docs/assets/favicon.ico +1 -0
- data/docs/assets/logo.svg +24 -0
- data/docs/core-features/comments.md +48 -0
- data/docs/core-features/directive-processing.md +38 -0
- data/docs/core-features/erb-integration.md +68 -0
- data/docs/core-features/error-handling.md +197 -0
- data/docs/core-features/parameter-history.md +76 -0
- data/docs/core-features/parameterized-prompts.md +500 -0
- data/docs/core-features/shell-integration.md +79 -0
- data/docs/development/architecture.md +544 -0
- data/docs/development/contributing.md +425 -0
- data/docs/development/roadmap.md +234 -0
- data/docs/development/testing.md +822 -0
- data/docs/examples/advanced.md +523 -0
- data/docs/examples/basic.md +688 -0
- data/docs/examples/real-world.md +776 -0
- data/docs/examples.md +337 -0
- data/docs/getting-started/basic-concepts.md +318 -0
- data/docs/getting-started/installation.md +97 -0
- data/docs/getting-started/quick-start.md +256 -0
- data/docs/index.md +230 -0
- data/docs/migration/v0.9.0.md +459 -0
- data/docs/migration/v1.0.0.md +591 -0
- data/docs/storage/activerecord-adapter.md +348 -0
- data/docs/storage/custom-adapters.md +176 -0
- data/docs/storage/filesystem-adapter.md +236 -0
- data/docs/storage/overview.md +427 -0
- data/examples/advanced_integrations.rb +52 -0
- data/examples/prompts_dir/advanced_demo.txt +79 -0
- data/examples/prompts_dir/directive_example.json +1 -0
- data/examples/prompts_dir/directive_example.txt +8 -0
- data/examples/prompts_dir/todo.json +1 -1
- data/improvement_plan.md +996 -0
- data/lib/prompt_manager/storage/file_system_adapter.rb +8 -2
- data/lib/prompt_manager/version.rb +1 -1
- data/mkdocs.yml +146 -0
- data/prompt_manager_logo.png +0 -0
- metadata +46 -3
- data/LICENSE.txt +0 -21
@@ -0,0 +1,635 @@
|
|
1
|
+
# Search Integration
|
2
|
+
|
3
|
+
PromptManager provides powerful search capabilities to find, filter, and organize prompts across your entire prompt library.
|
4
|
+
|
5
|
+
## Basic Search
|
6
|
+
|
7
|
+
### Simple Text Search
|
8
|
+
|
9
|
+
```ruby
|
10
|
+
# Search by prompt content
|
11
|
+
results = PromptManager.search("customer service")
|
12
|
+
results.each do |prompt_id|
|
13
|
+
puts "Found: #{prompt_id}"
|
14
|
+
end
|
15
|
+
|
16
|
+
# Search with options
|
17
|
+
results = PromptManager.search(
|
18
|
+
query: "email template",
|
19
|
+
limit: 10,
|
20
|
+
include_content: true
|
21
|
+
)
|
22
|
+
|
23
|
+
results.each do |result|
|
24
|
+
puts "ID: #{result[:id]}"
|
25
|
+
puts "Content: #{result[:content][0..100]}..."
|
26
|
+
end
|
27
|
+
```
|
28
|
+
|
29
|
+
### Search by Metadata
|
30
|
+
|
31
|
+
```ruby
|
32
|
+
# Search by prompt ID pattern
|
33
|
+
email_prompts = PromptManager.search(id_pattern: /email/)
|
34
|
+
|
35
|
+
# Search by file path (FileSystem adapter)
|
36
|
+
marketing_prompts = PromptManager.search(path_pattern: /marketing/)
|
37
|
+
|
38
|
+
# Search by tags (if using metadata)
|
39
|
+
customer_service = PromptManager.search(tags: ['customer-service', 'support'])
|
40
|
+
```
|
41
|
+
|
42
|
+
## Advanced Search Features
|
43
|
+
|
44
|
+
### Full-Text Search with Elasticsearch
|
45
|
+
|
46
|
+
```ruby
|
47
|
+
# config/initializers/prompt_manager.rb
|
48
|
+
PromptManager.configure do |config|
|
49
|
+
config.search_backend = PromptManager::Search::ElasticsearchBackend.new(
|
50
|
+
host: ENV['ELASTICSEARCH_URL'],
|
51
|
+
index: 'prompt_manager_prompts'
|
52
|
+
)
|
53
|
+
end
|
54
|
+
|
55
|
+
class PromptManager::Search::ElasticsearchBackend
|
56
|
+
def initialize(host:, index:)
|
57
|
+
@client = Elasticsearch::Client.new(hosts: host)
|
58
|
+
@index = index
|
59
|
+
setup_index
|
60
|
+
end
|
61
|
+
|
62
|
+
def search(query, options = {})
|
63
|
+
search_body = build_search_query(query, options)
|
64
|
+
|
65
|
+
response = @client.search(
|
66
|
+
index: @index,
|
67
|
+
body: search_body
|
68
|
+
)
|
69
|
+
|
70
|
+
parse_search_results(response)
|
71
|
+
end
|
72
|
+
|
73
|
+
def index_prompt(prompt_id, content, metadata = {})
|
74
|
+
document = {
|
75
|
+
id: prompt_id,
|
76
|
+
content: content,
|
77
|
+
metadata: metadata,
|
78
|
+
indexed_at: Time.current.iso8601,
|
79
|
+
parameters: extract_parameters(content),
|
80
|
+
directives: extract_directives(content)
|
81
|
+
}
|
82
|
+
|
83
|
+
@client.index(
|
84
|
+
index: @index,
|
85
|
+
id: prompt_id,
|
86
|
+
body: document
|
87
|
+
)
|
88
|
+
end
|
89
|
+
|
90
|
+
private
|
91
|
+
|
92
|
+
def build_search_query(query, options)
|
93
|
+
{
|
94
|
+
query: {
|
95
|
+
bool: {
|
96
|
+
should: [
|
97
|
+
{
|
98
|
+
match: {
|
99
|
+
content: {
|
100
|
+
query: query,
|
101
|
+
boost: 2.0
|
102
|
+
}
|
103
|
+
}
|
104
|
+
},
|
105
|
+
{
|
106
|
+
match: {
|
107
|
+
id: {
|
108
|
+
query: query,
|
109
|
+
boost: 1.5
|
110
|
+
}
|
111
|
+
}
|
112
|
+
},
|
113
|
+
{
|
114
|
+
nested: {
|
115
|
+
path: 'metadata',
|
116
|
+
query: {
|
117
|
+
match: {
|
118
|
+
'metadata.description': query
|
119
|
+
}
|
120
|
+
}
|
121
|
+
}
|
122
|
+
}
|
123
|
+
],
|
124
|
+
filter: build_filters(options)
|
125
|
+
}
|
126
|
+
},
|
127
|
+
highlight: {
|
128
|
+
fields: {
|
129
|
+
content: {},
|
130
|
+
id: {}
|
131
|
+
}
|
132
|
+
},
|
133
|
+
size: options[:limit] || 20,
|
134
|
+
from: options[:offset] || 0
|
135
|
+
}
|
136
|
+
end
|
137
|
+
end
|
138
|
+
```
|
139
|
+
|
140
|
+
### Faceted Search
|
141
|
+
|
142
|
+
```ruby
|
143
|
+
search_results = PromptManager.search(
|
144
|
+
query: "email",
|
145
|
+
facets: {
|
146
|
+
tags: {},
|
147
|
+
category: {},
|
148
|
+
last_modified: {
|
149
|
+
ranges: [
|
150
|
+
{ to: "now-1d", label: "Last 24 hours" },
|
151
|
+
{ from: "now-7d", to: "now-1d", label: "Last week" },
|
152
|
+
{ from: "now-30d", to: "now-7d", label: "Last month" }
|
153
|
+
]
|
154
|
+
}
|
155
|
+
}
|
156
|
+
)
|
157
|
+
|
158
|
+
puts "Results: #{search_results[:total]}"
|
159
|
+
puts "Facets:"
|
160
|
+
search_results[:facets].each do |facet_name, facet_data|
|
161
|
+
puts " #{facet_name}:"
|
162
|
+
facet_data[:buckets].each do |bucket|
|
163
|
+
puts " #{bucket[:label]}: #{bucket[:count]}"
|
164
|
+
end
|
165
|
+
end
|
166
|
+
```
|
167
|
+
|
168
|
+
### Semantic Search with Vector Embeddings
|
169
|
+
|
170
|
+
```ruby
|
171
|
+
class PromptManager::Search::VectorBackend
|
172
|
+
def initialize(embedding_model: 'text-embedding-ada-002')
|
173
|
+
@openai = OpenAI::Client.new
|
174
|
+
@embedding_model = embedding_model
|
175
|
+
@vector_db = Pinecone::Client.new
|
176
|
+
end
|
177
|
+
|
178
|
+
def index_prompt(prompt_id, content, metadata = {})
|
179
|
+
# Generate embedding
|
180
|
+
embedding_response = @openai.embeddings(
|
181
|
+
parameters: {
|
182
|
+
model: @embedding_model,
|
183
|
+
input: content
|
184
|
+
}
|
185
|
+
)
|
186
|
+
|
187
|
+
embedding = embedding_response['data'][0]['embedding']
|
188
|
+
|
189
|
+
# Store in vector database
|
190
|
+
@vector_db.upsert(
|
191
|
+
namespace: 'prompts',
|
192
|
+
vectors: [{
|
193
|
+
id: prompt_id,
|
194
|
+
values: embedding,
|
195
|
+
metadata: {
|
196
|
+
content: content,
|
197
|
+
**metadata
|
198
|
+
}
|
199
|
+
}]
|
200
|
+
)
|
201
|
+
end
|
202
|
+
|
203
|
+
def semantic_search(query, limit: 10, similarity_threshold: 0.8)
|
204
|
+
# Generate query embedding
|
205
|
+
query_embedding = @openai.embeddings(
|
206
|
+
parameters: {
|
207
|
+
model: @embedding_model,
|
208
|
+
input: query
|
209
|
+
}
|
210
|
+
)['data'][0]['embedding']
|
211
|
+
|
212
|
+
# Search for similar vectors
|
213
|
+
results = @vector_db.query(
|
214
|
+
namespace: 'prompts',
|
215
|
+
vector: query_embedding,
|
216
|
+
top_k: limit,
|
217
|
+
include_metadata: true
|
218
|
+
)
|
219
|
+
|
220
|
+
# Filter by similarity threshold
|
221
|
+
results['matches'].select do |match|
|
222
|
+
match['score'] >= similarity_threshold
|
223
|
+
end
|
224
|
+
end
|
225
|
+
end
|
226
|
+
|
227
|
+
# Usage
|
228
|
+
PromptManager.configure do |config|
|
229
|
+
config.search_backend = PromptManager::Search::VectorBackend.new
|
230
|
+
end
|
231
|
+
|
232
|
+
# Find semantically similar prompts
|
233
|
+
similar_prompts = PromptManager.semantic_search(
|
234
|
+
"greeting message for new customers",
|
235
|
+
limit: 5
|
236
|
+
)
|
237
|
+
|
238
|
+
similar_prompts.each do |result|
|
239
|
+
puts "#{result['id']} (similarity: #{result['score']})"
|
240
|
+
puts result['metadata']['content'][0..100]
|
241
|
+
puts "---"
|
242
|
+
end
|
243
|
+
```
|
244
|
+
|
245
|
+
## Search Integration Patterns
|
246
|
+
|
247
|
+
### Auto-completion and Suggestions
|
248
|
+
|
249
|
+
```ruby
|
250
|
+
class PromptSearchController < ApplicationController
|
251
|
+
def autocomplete
|
252
|
+
query = params[:q]
|
253
|
+
suggestions = PromptManager.search(
|
254
|
+
query: query,
|
255
|
+
type: :autocomplete,
|
256
|
+
limit: 10
|
257
|
+
)
|
258
|
+
|
259
|
+
render json: {
|
260
|
+
suggestions: suggestions.map do |result|
|
261
|
+
{
|
262
|
+
id: result[:id],
|
263
|
+
title: result[:title] || result[:id].humanize,
|
264
|
+
description: result[:content][0..100],
|
265
|
+
category: result[:metadata][:category]
|
266
|
+
}
|
267
|
+
end
|
268
|
+
}
|
269
|
+
end
|
270
|
+
|
271
|
+
def search
|
272
|
+
results = PromptManager.search(
|
273
|
+
query: params[:q],
|
274
|
+
filters: search_filters,
|
275
|
+
facets: search_facets,
|
276
|
+
page: params[:page] || 1,
|
277
|
+
per_page: 20
|
278
|
+
)
|
279
|
+
|
280
|
+
render json: {
|
281
|
+
results: results[:items],
|
282
|
+
total: results[:total],
|
283
|
+
facets: results[:facets],
|
284
|
+
pagination: {
|
285
|
+
page: params[:page]&.to_i || 1,
|
286
|
+
total_pages: (results[:total] / 20.0).ceil
|
287
|
+
}
|
288
|
+
}
|
289
|
+
end
|
290
|
+
|
291
|
+
private
|
292
|
+
|
293
|
+
def search_filters
|
294
|
+
filters = {}
|
295
|
+
filters[:category] = params[:category] if params[:category].present?
|
296
|
+
filters[:tags] = params[:tags].split(',') if params[:tags].present?
|
297
|
+
filters[:date_range] = params[:date_range] if params[:date_range].present?
|
298
|
+
filters
|
299
|
+
end
|
300
|
+
end
|
301
|
+
```
|
302
|
+
|
303
|
+
### Search Analytics
|
304
|
+
|
305
|
+
```ruby
|
306
|
+
class SearchAnalytics
|
307
|
+
def self.track_search(query, user_id, results_count)
|
308
|
+
SearchLog.create!(
|
309
|
+
query: query,
|
310
|
+
user_id: user_id,
|
311
|
+
results_count: results_count,
|
312
|
+
searched_at: Time.current
|
313
|
+
)
|
314
|
+
end
|
315
|
+
|
316
|
+
def self.popular_searches(limit: 10)
|
317
|
+
SearchLog
|
318
|
+
.where('searched_at > ?', 30.days.ago)
|
319
|
+
.group(:query)
|
320
|
+
.order('count_all DESC')
|
321
|
+
.limit(limit)
|
322
|
+
.count
|
323
|
+
end
|
324
|
+
|
325
|
+
def self.search_trends
|
326
|
+
SearchLog
|
327
|
+
.where('searched_at > ?', 7.days.ago)
|
328
|
+
.group('DATE(searched_at)')
|
329
|
+
.count
|
330
|
+
end
|
331
|
+
|
332
|
+
def self.no_results_queries
|
333
|
+
SearchLog
|
334
|
+
.where(results_count: 0)
|
335
|
+
.where('searched_at > ?', 7.days.ago)
|
336
|
+
.group(:query)
|
337
|
+
.order('count_all DESC')
|
338
|
+
.limit(20)
|
339
|
+
.count
|
340
|
+
end
|
341
|
+
end
|
342
|
+
|
343
|
+
# Usage in search
|
344
|
+
results = PromptManager.search(params[:q])
|
345
|
+
SearchAnalytics.track_search(params[:q], current_user.id, results.count)
|
346
|
+
```
|
347
|
+
|
348
|
+
### Search Result Ranking
|
349
|
+
|
350
|
+
```ruby
|
351
|
+
class PromptRankingService
|
352
|
+
RANKING_FACTORS = {
|
353
|
+
exact_match: 3.0,
|
354
|
+
title_match: 2.0,
|
355
|
+
content_relevance: 1.0,
|
356
|
+
recency: 0.5,
|
357
|
+
usage_frequency: 1.5,
|
358
|
+
user_preference: 2.0
|
359
|
+
}.freeze
|
360
|
+
|
361
|
+
def self.rank_results(results, query, user_context = {})
|
362
|
+
scored_results = results.map do |result|
|
363
|
+
score = calculate_score(result, query, user_context)
|
364
|
+
result.merge(relevance_score: score)
|
365
|
+
end
|
366
|
+
|
367
|
+
scored_results.sort_by { |r| -r[:relevance_score] }
|
368
|
+
end
|
369
|
+
|
370
|
+
private
|
371
|
+
|
372
|
+
def self.calculate_score(result, query, user_context)
|
373
|
+
score = 0.0
|
374
|
+
|
375
|
+
# Exact match bonus
|
376
|
+
if result[:id].downcase.include?(query.downcase)
|
377
|
+
score += RANKING_FACTORS[:exact_match]
|
378
|
+
end
|
379
|
+
|
380
|
+
# Content relevance
|
381
|
+
content_matches = result[:content].downcase.scan(query.downcase).size
|
382
|
+
score += content_matches * RANKING_FACTORS[:content_relevance]
|
383
|
+
|
384
|
+
# Recency bonus
|
385
|
+
days_old = (Time.current - result[:updated_at]).to_f / 1.day
|
386
|
+
recency_factor = [1.0 - (days_old / 365.0), 0.0].max
|
387
|
+
score += recency_factor * RANKING_FACTORS[:recency]
|
388
|
+
|
389
|
+
# Usage frequency
|
390
|
+
usage_count = PromptUsageLog.where(prompt_id: result[:id])
|
391
|
+
.where('used_at > ?', 30.days.ago)
|
392
|
+
.count
|
393
|
+
score += Math.log(usage_count + 1) * RANKING_FACTORS[:usage_frequency]
|
394
|
+
|
395
|
+
# User preference (based on past usage)
|
396
|
+
if user_context[:user_id]
|
397
|
+
user_usage = PromptUsageLog.where(
|
398
|
+
prompt_id: result[:id],
|
399
|
+
user_id: user_context[:user_id]
|
400
|
+
).count
|
401
|
+
score += Math.log(user_usage + 1) * RANKING_FACTORS[:user_preference]
|
402
|
+
end
|
403
|
+
|
404
|
+
score
|
405
|
+
end
|
406
|
+
end
|
407
|
+
```
|
408
|
+
|
409
|
+
## Search UI Components
|
410
|
+
|
411
|
+
### React Search Component
|
412
|
+
|
413
|
+
```jsx
|
414
|
+
// components/PromptSearch.jsx
|
415
|
+
import React, { useState, useEffect, useMemo } from 'react';
|
416
|
+
import { debounce } from 'lodash';
|
417
|
+
|
418
|
+
const PromptSearch = () => {
|
419
|
+
const [query, setQuery] = useState('');
|
420
|
+
const [results, setResults] = useState([]);
|
421
|
+
const [facets, setFacets] = useState({});
|
422
|
+
const [selectedFilters, setSelectedFilters] = useState({});
|
423
|
+
const [loading, setLoading] = useState(false);
|
424
|
+
|
425
|
+
const debouncedSearch = useMemo(
|
426
|
+
() => debounce(async (searchQuery, filters) => {
|
427
|
+
setLoading(true);
|
428
|
+
try {
|
429
|
+
const response = await fetch('/api/prompts/search', {
|
430
|
+
method: 'POST',
|
431
|
+
headers: {
|
432
|
+
'Content-Type': 'application/json',
|
433
|
+
},
|
434
|
+
body: JSON.stringify({
|
435
|
+
query: searchQuery,
|
436
|
+
filters: filters
|
437
|
+
}),
|
438
|
+
});
|
439
|
+
|
440
|
+
const data = await response.json();
|
441
|
+
setResults(data.results);
|
442
|
+
setFacets(data.facets);
|
443
|
+
} catch (error) {
|
444
|
+
console.error('Search error:', error);
|
445
|
+
} finally {
|
446
|
+
setLoading(false);
|
447
|
+
}
|
448
|
+
}, 300),
|
449
|
+
[]
|
450
|
+
);
|
451
|
+
|
452
|
+
useEffect(() => {
|
453
|
+
if (query.length > 2) {
|
454
|
+
debouncedSearch(query, selectedFilters);
|
455
|
+
} else {
|
456
|
+
setResults([]);
|
457
|
+
}
|
458
|
+
}, [query, selectedFilters, debouncedSearch]);
|
459
|
+
|
460
|
+
return (
|
461
|
+
<div className="prompt-search">
|
462
|
+
<div className="search-input">
|
463
|
+
<input
|
464
|
+
type="text"
|
465
|
+
value={query}
|
466
|
+
onChange={(e) => setQuery(e.target.value)}
|
467
|
+
placeholder="Search prompts..."
|
468
|
+
className="search-field"
|
469
|
+
/>
|
470
|
+
{loading && <div className="search-spinner">Loading...</div>}
|
471
|
+
</div>
|
472
|
+
|
473
|
+
<div className="search-content">
|
474
|
+
<div className="search-filters">
|
475
|
+
{Object.entries(facets).map(([facetName, facetData]) => (
|
476
|
+
<div key={facetName} className="facet-group">
|
477
|
+
<h4>{facetName.charAt(0).toUpperCase() + facetName.slice(1)}</h4>
|
478
|
+
{facetData.buckets.map(bucket => (
|
479
|
+
<label key={bucket.key} className="facet-option">
|
480
|
+
<input
|
481
|
+
type="checkbox"
|
482
|
+
checked={selectedFilters[facetName]?.includes(bucket.key) || false}
|
483
|
+
onChange={(e) => handleFilterChange(facetName, bucket.key, e.target.checked)}
|
484
|
+
/>
|
485
|
+
{bucket.label} ({bucket.count})
|
486
|
+
</label>
|
487
|
+
))}
|
488
|
+
</div>
|
489
|
+
))}
|
490
|
+
</div>
|
491
|
+
|
492
|
+
<div className="search-results">
|
493
|
+
{results.map(result => (
|
494
|
+
<div key={result.id} className="search-result">
|
495
|
+
<h3 className="result-title">{result.title || result.id}</h3>
|
496
|
+
<p className="result-content">{result.snippet}</p>
|
497
|
+
<div className="result-metadata">
|
498
|
+
<span className="result-category">{result.category}</span>
|
499
|
+
<span className="result-score">Score: {result.relevance_score?.toFixed(2)}</span>
|
500
|
+
</div>
|
501
|
+
</div>
|
502
|
+
))}
|
503
|
+
</div>
|
504
|
+
</div>
|
505
|
+
</div>
|
506
|
+
);
|
507
|
+
};
|
508
|
+
|
509
|
+
export default PromptSearch;
|
510
|
+
```
|
511
|
+
|
512
|
+
### Search API Implementation
|
513
|
+
|
514
|
+
```ruby
|
515
|
+
class Api::PromptsController < ApplicationController
|
516
|
+
def search
|
517
|
+
search_params = params.require(:search).permit(
|
518
|
+
:query, :page, :per_page,
|
519
|
+
filters: {},
|
520
|
+
facets: []
|
521
|
+
)
|
522
|
+
|
523
|
+
results = PromptManager.search(
|
524
|
+
query: search_params[:query],
|
525
|
+
filters: search_params[:filters] || {},
|
526
|
+
facets: search_params[:facets] || [],
|
527
|
+
page: search_params[:page]&.to_i || 1,
|
528
|
+
per_page: [search_params[:per_page]&.to_i || 20, 100].min
|
529
|
+
)
|
530
|
+
|
531
|
+
# Apply custom ranking
|
532
|
+
ranked_results = PromptRankingService.rank_results(
|
533
|
+
results[:items],
|
534
|
+
search_params[:query],
|
535
|
+
user_context: { user_id: current_user&.id }
|
536
|
+
)
|
537
|
+
|
538
|
+
render json: {
|
539
|
+
results: ranked_results,
|
540
|
+
total: results[:total],
|
541
|
+
facets: results[:facets],
|
542
|
+
query: search_params[:query]
|
543
|
+
}
|
544
|
+
end
|
545
|
+
|
546
|
+
def suggestions
|
547
|
+
query = params[:q]
|
548
|
+
|
549
|
+
suggestions = PromptManager.search(
|
550
|
+
query: query,
|
551
|
+
type: :suggestions,
|
552
|
+
limit: 8
|
553
|
+
)
|
554
|
+
|
555
|
+
render json: {
|
556
|
+
suggestions: suggestions.map do |s|
|
557
|
+
{
|
558
|
+
text: s[:id].humanize,
|
559
|
+
value: s[:id],
|
560
|
+
category: s[:metadata][:category]
|
561
|
+
}
|
562
|
+
end
|
563
|
+
}
|
564
|
+
end
|
565
|
+
end
|
566
|
+
```
|
567
|
+
|
568
|
+
## Performance Optimization
|
569
|
+
|
570
|
+
### Search Indexing Strategy
|
571
|
+
|
572
|
+
```ruby
|
573
|
+
class PromptIndexer
|
574
|
+
def self.reindex_all
|
575
|
+
total_prompts = PromptManager.list.count
|
576
|
+
|
577
|
+
PromptManager.list.each_with_index do |prompt_id, index|
|
578
|
+
begin
|
579
|
+
prompt = PromptManager::Prompt.new(id: prompt_id)
|
580
|
+
content = prompt.content
|
581
|
+
metadata = extract_metadata(prompt)
|
582
|
+
|
583
|
+
PromptManager.search_backend.index_prompt(
|
584
|
+
prompt_id,
|
585
|
+
content,
|
586
|
+
metadata
|
587
|
+
)
|
588
|
+
|
589
|
+
puts "Indexed #{index + 1}/#{total_prompts}: #{prompt_id}"
|
590
|
+
rescue => e
|
591
|
+
Rails.logger.error "Failed to index #{prompt_id}: #{e.message}"
|
592
|
+
end
|
593
|
+
end
|
594
|
+
end
|
595
|
+
|
596
|
+
def self.incremental_index
|
597
|
+
# Index only recently modified prompts
|
598
|
+
recently_modified = PromptManager.list.select do |prompt_id|
|
599
|
+
prompt = PromptManager::Prompt.new(id: prompt_id)
|
600
|
+
last_modified = File.mtime(prompt.file_path) rescue Time.at(0)
|
601
|
+
last_indexed = IndexLog.where(prompt_id: prompt_id).maximum(:indexed_at) || Time.at(0)
|
602
|
+
|
603
|
+
last_modified > last_indexed
|
604
|
+
end
|
605
|
+
|
606
|
+
recently_modified.each do |prompt_id|
|
607
|
+
index_single_prompt(prompt_id)
|
608
|
+
end
|
609
|
+
end
|
610
|
+
end
|
611
|
+
|
612
|
+
# Schedule regular reindexing
|
613
|
+
class PromptReindexJob < ApplicationJob
|
614
|
+
def perform
|
615
|
+
PromptIndexer.incremental_index
|
616
|
+
end
|
617
|
+
end
|
618
|
+
|
619
|
+
# Run every hour
|
620
|
+
# schedule.rb or similar
|
621
|
+
every 1.hour do
|
622
|
+
PromptReindexJob.perform_later
|
623
|
+
end
|
624
|
+
```
|
625
|
+
|
626
|
+
## Best Practices
|
627
|
+
|
628
|
+
1. **Index Management**: Keep search indexes up to date with prompt changes
|
629
|
+
2. **Query Optimization**: Use proper filters and pagination to improve performance
|
630
|
+
3. **Result Ranking**: Implement relevance scoring based on user behavior
|
631
|
+
4. **Analytics**: Track search patterns to improve the search experience
|
632
|
+
5. **Faceted Navigation**: Provide filters to help users narrow down results
|
633
|
+
6. **Error Handling**: Gracefully handle search backend failures
|
634
|
+
7. **Caching**: Cache frequent searches and autocomplete suggestions
|
635
|
+
8. **Security**: Ensure search queries are properly sanitized and authorized
|