ruby
54 lines · 9 steps
Building search-result snippets in Ruby
A class that finds a query match in text and returns a trimmed, highlighted excerpt around it.
Explained by
highlit
1class SnippetHighlighter
2 CONTEXT_RADIUS = 60
3 MAX_TERMS = 8
4
5 def initialize(text, query)
6 @text = text.to_s
7 @terms = extract_terms(query)
8 end
9
10 def call
11 return truncated_lead if @terms.empty?
12
13 match = first_match
14 return truncated_lead unless match
15
16 window = build_window(match.begin(0), match.end(0))
17 highlight(window)
18 end
19
20 private
21
22 def extract_terms(query)
23 query.to_s.scan(/\w+/).uniq.first(MAX_TERMS).map(&:downcase)
24 end
25
26 def pattern
27 @pattern ||= Regexp.union(@terms.map { |t| /#{Regexp.escape(t)}/i })
28 end
29
30 def first_match
31 pattern.match(@text)
32 end
33
34 def build_window(match_start, match_end)
35 from = [match_start - CONTEXT_RADIUS, 0].max
36 to = [match_end + CONTEXT_RADIUS, @text.length].min
37
38 from -= 1 until from.zero? || @text[from - 1] =~ /\s/
39 to += 1 until to == @text.length || @text[to] =~ /\s/
40
41 prefix = from.zero? ? "" : "\u2026"
42 suffix = to == @text.length ? "" : "\u2026"
43 "#{prefix}#{@text[from...to].strip}#{suffix}"
44 end
45
46 def highlight(window)
47 window.gsub(pattern) { |m| "<mark>#{m}</mark>" }
48 end
49
50 def truncated_lead
51 lead = @text[0, CONTEXT_RADIUS * 2].to_s.strip
52 @text.length > lead.length ? "#{lead}\u2026" : lead
53 end
54end
01 / 01
STEP 01
‹ swipe to step through ›
Walkthrough
Space play
←→ step
click any line
Three takeaways
- 1Clamping offsets with max and min keeps window boundaries safely inside the string.
- 2Regexp.union with escaped, case-insensitive terms matches any query word in one pass.
- 3Graceful fallbacks — an empty query or no match — keep a snippet builder from ever returning nothing useful.
Related explainers
ruby
require "shellwords" require "open3" module Backup
Building safe shell commands in Ruby
shell-out
subprocess
command-injection
Intermediate
7 steps
ruby
class UserAgentParser BROWSERS = [ [/Edg\/([\d.]+)/, "Edge"], [/OPR\/([\d.]+)/, "Opera"],
Parsing user-agent strings in Ruby
regex
pattern-matching
lookup-tables
Intermediate
8 steps
php
<?php namespace App\Services;
How a password strength validator works in PHP
validation
regular-expressions
data-driven
Intermediate
8 steps
ruby
class LogAggregator BUCKET_FORMAT = "%Y-%m-%dT%H:%M" def initialize(entries)
Bucketing log entries by the minute in Ruby
aggregation
hashing
enumerable
Intermediate
5 steps
ruby
class WeeklySignupsReport DEFAULT_WEEKS = 12 def initialize(weeks: DEFAULT_WEEKS, source: User.all)
Building a weekly signups report in Rails
service object
aggregation
group by
Intermediate
7 steps
ruby
class ApplicationController < ActionController::Base EXPERIMENTS = { checkout_button_color: %w[control blue green], onboarding_flow: %w[control streamlined]
How A/B test cohorts are assigned in Rails
a-b-testing
cookies
hashing
Intermediate
8 steps
Share this explainer
Here's the card — post it anywhere.
Made with highlit — turn any snippet into a walkthrough like this in about a minute.
Explain your code
Embed this explainer
Drop the interactive walkthrough into a blog or docs. Views never cost a credit.
<iframe src="https://highlit.co/explainers/building-search-result-snippets-in-ruby-explained-ruby-ee36/embed?autoplay=1" width="100%" height="520" loading="lazy" style="border:0"></iframe>
Autoplay is on by default — add ?autoplay=0 to start paused.