Skip to content

Commit 5abfc1b

Browse files
Kalyan Kanuriclaude
andcommitted
fix: Address voidmatcha review feedback + UI improvements
- Display paragraph title in search results (Angular + Classic UI) - Dark mode: replace hardcoded colors with themeMixin design tokens - Prettier: reformat tablesText chain to multi-line - Hybrid search: add keyword boost so exact matches surface even when embedding similarity is low (e.g. searching "TETRIS" finds SQL containing TETRIS_VIDEO_SINGLE_MEDIA) - Highlight query terms in semantic search snippets using <B> tags - Make entire search result card clickable (preserves text selection) - Deep link: wire term query param to highlight matches in editor - Search history: persist recent searches via localStorage + datalist - Fix InterpreterFactory NPE when interpreter group is not installed Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
1 parent 0287bb5 commit 5abfc1b

10 files changed

Lines changed: 173 additions & 49 deletions

File tree

zeppelin-server/src/main/java/org/apache/zeppelin/interpreter/InterpreterFactory.java

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,10 @@ public Interpreter getInterpreter(String replName,
4444
// Get the default interpreter of the defaultInterpreterSetting
4545
InterpreterSetting defaultSetting =
4646
interpreterSettingManager.getByName(executionContext.getDefaultInterpreterGroup());
47+
if (defaultSetting == null) {
48+
throw new InterpreterNotFoundException("No interpreter found for group: "
49+
+ executionContext.getDefaultInterpreterGroup());
50+
}
4751
return defaultSetting.getDefaultInterpreter(executionContext);
4852
}
4953

zeppelin-server/src/main/java/org/apache/zeppelin/search/EmbeddingSearch.java

Lines changed: 36 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,7 @@
3636
import java.security.MessageDigest;
3737
import java.security.NoSuchAlgorithmException;
3838
import java.util.ArrayList;
39+
import java.util.Locale;
3940
import java.util.Collections;
4041
import java.util.HashMap;
4142
import java.util.HashSet;
@@ -106,6 +107,12 @@ public class EmbeddingSearch extends SearchService {
106107
* and cannot promote semantically unrelated results past {@link #MIN_SIMILARITY}.
107108
*/
108109
private static final float TABLE_BOOST = 0.05f;
110+
/**
111+
* Additive score boost when the query string appears literally in the indexed text.
112+
* Ensures exact keyword matches surface even when the embedding similarity is low
113+
* (e.g. searching "TETRIS" in SQL containing TETRIS_VIDEO_SINGLE_MEDIA).
114+
*/
115+
private static final float KEYWORD_BOOST = 0.30f;
109116
/**
110117
* Fraction of the top table's weight used as the cutoff for "relevant" tables in Phase 1
111118
* of {@link #query(String)}. Tables below this share are dropped from the boost set
@@ -380,6 +387,25 @@ private static float cosineSimilarity(float[] a, float[] b) {
380387
return dot;
381388
}
382389

390+
/**
391+
* Wrap occurrences of each query word in {@code <B>} tags (case-insensitive)
392+
* to match Lucene's highlighting convention.
393+
*/
394+
static String highlightTerms(String text, String queryStr) {
395+
if (StringUtils.isBlank(text) || StringUtils.isBlank(queryStr)) {
396+
return text;
397+
}
398+
String[] words = queryStr.split("\\s+");
399+
for (String word : words) {
400+
if (word.isEmpty()) {
401+
continue;
402+
}
403+
String escaped = Pattern.quote(word);
404+
text = text.replaceAll("(?i)(" + escaped + ")", "<B>$1</B>");
405+
}
406+
return text;
407+
}
408+
383409
// ---- Text extraction ----
384410

385411
/**
@@ -494,13 +520,18 @@ public List<Map<String, String>> query(String queryStr) {
494520
}
495521

496522
float[] queryEmbedding = embed(queryStr);
523+
String queryLower = queryStr.toLowerCase(Locale.ROOT);
497524

498525
// Phase 1: find top-N results and discover relevant tables
499526
List<Map.Entry<String, Float>> scored = new ArrayList<>();
500527
indexLock.readLock().lock();
501528
try {
502529
for (Map.Entry<String, IndexEntry> entry : index.entrySet()) {
503530
float sim = cosineSimilarity(queryEmbedding, entry.getValue().embedding);
531+
IndexEntry ie = entry.getValue();
532+
if (ie.text != null && ie.text.toLowerCase(Locale.ROOT).contains(queryLower)) {
533+
sim += KEYWORD_BOOST;
534+
}
504535
scored.add(Map.entry(entry.getKey(), sim));
505536
}
506537
} finally {
@@ -559,13 +590,15 @@ public List<Map<String, String>> query(String queryStr) {
559590
output = output.substring(0, 300);
560591
}
561592
}
593+
String snippet = highlightTerms(entry.text, queryStr);
594+
String highlightedTitle = highlightTerms(title, queryStr);
562595
candidates.add(Map.entry(ImmutableMap.<String, String>builder()
563596
.put("id", docId)
564597
.put("name", entry.noteName != null ? entry.noteName : "")
565-
.put("snippet", entry.text)
598+
.put("snippet", snippet)
566599
.put("text", entry.text)
567-
.put("header", title)
568-
.put("title", title)
600+
.put("header", highlightedTitle)
601+
.put("title", highlightedTitle)
569602
.put("tables", tables)
570603
.put("output", output)
571604
.build(), sim));

zeppelin-web-angular/src/app/pages/workspace/notebook-search/result-item/result-item.component.html

Lines changed: 6 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -10,13 +10,12 @@
1010
~ limitations under the License.
1111
-->
1212

13-
<nz-card [nzTitle]="titleTemplateRef">
14-
<ng-template #titleTemplateRef>
15-
<div class="result-header">
16-
<a [routerLink]="routerLink" [queryParams]="queryParams">{{ displayName }}</a>
17-
<span *ngIf="interpreter" class="badge" [ngClass]="interpreter">{{ interpreter }}</span>
18-
</div>
19-
</ng-template>
13+
<nz-card class="result-card" (click)="navigate($event)">
14+
<div class="result-header">
15+
<a [routerLink]="routerLink" [queryParams]="queryParams" (click)="$event.stopPropagation()">{{ displayName }}</a>
16+
<span *ngIf="interpreter" class="badge" [ngClass]="interpreter">{{ interpreter }}</span>
17+
</div>
18+
<div *ngIf="titleHtml" class="title-block" [innerHTML]="titleHtml"></div>
2019
<div *ngIf="codeHtml" class="code-block">
2120
<pre [innerHTML]="codeHtml"></pre>
2221
</div>

zeppelin-web-angular/src/app/pages/workspace/notebook-search/result-item/result-item.component.less

Lines changed: 67 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -10,11 +10,18 @@
1010
* limitations under the License.
1111
*/
1212

13+
@import 'theme-mixin';
14+
1315
:host {
1416
display: block;
1517
margin-bottom: 12px;
1618
}
1719

20+
.result-card {
21+
cursor: pointer;
22+
user-select: text;
23+
}
24+
1825
.result-header {
1926
display: flex;
2027
align-items: center;
@@ -25,39 +32,18 @@
2532
font-size: 11px;
2633
padding: 1px 8px;
2734
border-radius: 10px;
28-
background: #e8e8e8;
29-
color: #666;
30-
}
31-
32-
.badge.sql {
33-
background: #e6f7e6;
34-
color: #389e0d;
35-
}
36-
37-
.badge.python, .badge.pyspark {
38-
background: #fff7e6;
39-
color: #d48806;
40-
}
41-
42-
.badge.md {
43-
background: #e6f0ff;
44-
color: #1890ff;
4535
}
4636

4737
.code-block {
48-
background: #f6f8fa;
49-
border: 1px solid #e1e4e8;
5038
border-radius: 6px;
5139
padding: 10px 12px;
5240
margin-bottom: 8px;
5341
overflow-x: auto;
5442

5543
pre {
5644
margin: 0;
57-
font-family: 'SFMono-Regular', Consolas, 'Liberation Mono', Menlo, monospace;
5845
font-size: 12px;
5946
line-height: 1.5;
60-
color: #24292e;
6147
white-space: pre-wrap;
6248
word-break: break-word;
6349
max-height: 200px;
@@ -66,34 +52,88 @@
6652
}
6753

6854
.output-block {
69-
background: #fafbfc;
70-
border-left: 3px solid #d1d5da;
7155
border-radius: 0 4px 4px 0;
7256
padding: 8px 12px;
7357
margin-bottom: 8px;
7458
overflow-x: auto;
7559

7660
pre {
7761
margin: 0;
78-
font-family: 'SFMono-Regular', Consolas, 'Liberation Mono', Menlo, monospace;
7962
font-size: 11px;
8063
line-height: 1.4;
81-
color: #586069;
8264
white-space: pre-wrap;
8365
word-break: break-word;
8466
max-height: 120px;
8567
overflow-y: auto;
8668
}
8769
}
8870

71+
.title-block {
72+
font-size: 12px;
73+
padding: 4px 0;
74+
margin-bottom: 4px;
75+
}
76+
8977
.tables-block {
9078
font-size: 12px;
91-
color: #22863a;
9279
padding: 4px 0;
9380
}
9481

9582
mark {
96-
background-color: #fff3bf;
9783
padding: 0 1px;
9884
border-radius: 2px;
9985
}
86+
87+
.themeMixin({
88+
.badge {
89+
background: @background-color-base;
90+
color: @text-color-secondary;
91+
}
92+
93+
.badge.sql {
94+
background: @green-1;
95+
color: @green-7;
96+
}
97+
98+
.badge.python, .badge.pyspark {
99+
background: @gold-1;
100+
color: @gold-7;
101+
}
102+
103+
.badge.md {
104+
background: @blue-1;
105+
color: @blue-6;
106+
}
107+
108+
.code-block {
109+
background: @background-color-light;
110+
border: 1px solid @border-color-split;
111+
112+
pre {
113+
font-family: @code-family;
114+
color: @text-color;
115+
}
116+
}
117+
118+
.output-block {
119+
background: @background-color-light;
120+
border-left: 3px solid @border-color-base;
121+
122+
pre {
123+
font-family: @code-family;
124+
color: @text-color-secondary;
125+
}
126+
}
127+
128+
.title-block {
129+
color: @text-color-secondary;
130+
}
131+
132+
.tables-block {
133+
color: @green-7;
134+
}
135+
136+
mark {
137+
background-color: @gold-1;
138+
}
139+
});

zeppelin-web-angular/src/app/pages/workspace/notebook-search/result-item/result-item.component.ts

Lines changed: 24 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@
1111
*/
1212

1313
import { ChangeDetectionStrategy, Component, Input, OnChanges, SimpleChanges } from '@angular/core';
14-
import { ActivatedRoute } from '@angular/router';
14+
import { ActivatedRoute, Router } from '@angular/router';
1515
import { NotebookSearchResultItem } from '@zeppelin/interfaces';
1616

1717
@Component({
@@ -29,18 +29,31 @@ export class NotebookSearchResultItemComponent implements OnChanges {
2929
codeHtml = '';
3030
outputText = '';
3131
tablesText = '';
32+
titleHtml = '';
3233
interpreter = '';
3334

34-
constructor(private router: ActivatedRoute) {}
35+
constructor(
36+
private route: ActivatedRoute,
37+
private router: Router
38+
) {}
3539

3640
ngOnChanges(changes: SimpleChanges): void {
3741
if (changes.result) {
3842
this.parseResult();
3943
}
4044
}
4145

46+
navigate(event: MouseEvent): void {
47+
const selection = window.getSelection();
48+
if (selection && selection.toString().length > 0) {
49+
return;
50+
}
51+
event.preventDefault();
52+
this.router.navigate(this.routerLink, { queryParams: this.queryParams });
53+
}
54+
4255
private parseResult(): void {
43-
const term = this.router.snapshot.params.queryStr;
56+
const term = this.route.snapshot.params.queryStr;
4457
const listOfId = this.result.id.split('/');
4558
const [noteId, hasParagraph, paragraph] = listOfId;
4659
if (!hasParagraph) {
@@ -58,8 +71,15 @@ export class NotebookSearchResultItemComponent implements OnChanges {
5871
this.codeText = snippet.replace(/<\/?B>/gi, '');
5972
this.interpreter = this.detectInterpreter(this.codeText);
6073

74+
const title = this.result.title || '';
75+
this.titleHtml = title.replace(/<B>/gi, '<mark>').replace(/<\/B>/gi, '</mark>');
76+
6177
const tables = this.result.tables || '';
62-
this.tablesText = tables.trim().split(/\s+/).filter(t => t).join(', ');
78+
this.tablesText = tables
79+
.trim()
80+
.split(/\s+/)
81+
.filter(t => t)
82+
.join(', ');
6383
this.outputText = this.result.output || '';
6484
}
6585

zeppelin-web-angular/src/app/pages/workspace/notebook/notebook.component.ts

Lines changed: 5 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,7 @@ import { Title } from '@angular/platform-browser';
2323
import { ActivatedRoute, Router } from '@angular/router';
2424
import { isNil } from 'lodash';
2525
import { Subject } from 'rxjs';
26-
import { distinctUntilKeyChanged, map, startWith, takeUntil } from 'rxjs/operators';
26+
import { distinctUntilKeyChanged, startWith, takeUntil } from 'rxjs/operators';
2727

2828
import { NzResizeEvent } from 'ng-zorro-antd/resizable';
2929

@@ -422,14 +422,12 @@ export class NotebookComponent extends MessageListenersManager implements OnInit
422422

423423
ngOnInit() {
424424
this.activatedRoute.queryParamMap
425-
.pipe(
426-
startWith(this.activatedRoute.snapshot.queryParamMap),
427-
takeUntil(this.destroy$),
428-
map(data => data.get('paragraph'))
429-
)
430-
.subscribe(id => {
425+
.pipe(startWith(this.activatedRoute.snapshot.queryParamMap), takeUntil(this.destroy$))
426+
.subscribe(params => {
427+
const id = params.get('paragraph');
431428
this.onParagraphSelect(id);
432429
this.onParagraphScrolled(id);
430+
this.onParagraphSearch(params.get('term') || '');
433431
});
434432
this.activatedRoute.params.pipe(takeUntil(this.destroy$), distinctUntilKeyChanged('noteId')).subscribe(() => {
435433
this.noteVarShareService.clear();

zeppelin-web-angular/src/app/share/header/header.component.html

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -78,8 +78,18 @@
7878
</div>
7979
<div class="search">
8080
<nz-input-group [nzPrefixIcon]="'search'">
81-
<input type="text" nz-input placeholder="Search" (keyup.enter)="onSearch()" [(ngModel)]="queryStr" />
81+
<input
82+
type="text"
83+
nz-input
84+
placeholder="Search"
85+
list="search-history"
86+
(keyup.enter)="onSearch()"
87+
[(ngModel)]="queryStr"
88+
/>
8289
</nz-input-group>
90+
<datalist id="search-history">
91+
<option *ngFor="let item of searchHistory" [value]="item"></option>
92+
</datalist>
8393
</div>
8494
<zeppelin-theme-toggle class="theme-toggle"></zeppelin-theme-toggle>
8595
</div>

0 commit comments

Comments
 (0)