Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
71 changes: 71 additions & 0 deletions client/src/components/schema/PredicatePropertiesPanel.js
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,30 @@ export default class PredicatePropertiesPanel extends React.Component {
deleting: false,
errorMsg: '',
predicateQuery: null,
currentType: null, // Track type changes from form
cachedIndexSpecs: null, // Cache index_specs for restoration
}

componentDidMount() {
const { predicate } = this.props
this.setState({
currentType: predicate.type,
cachedIndexSpecs: predicate.index_specs,
})
}

componentDidUpdate(prevProps) {
// Reset when a different predicate is selected
if (prevProps.predicate?.predicate !== this.props.predicate?.predicate) {
this.setState({
currentType: this.props.predicate.type,
cachedIndexSpecs: this.props.predicate.index_specs,
})
}
}

handleTypeChange = (newType) => {
this.setState({ currentType: newType })
}

async handleUpdatePredicate() {
Expand Down Expand Up @@ -74,6 +98,44 @@ export default class PredicatePropertiesPanel extends React.Component {
}
}

renderIndexSpecs() {
const { currentType, cachedIndexSpecs } = this.state
if (
currentType !== 'float32vector' ||
!cachedIndexSpecs ||
cachedIndexSpecs.length === 0
) {
return null
}

return (
<div className='col-sm-12 mt-3 mb-3'>
<h6>Vector Index Options</h6>
{cachedIndexSpecs.map((spec, idx) => (
<div key={idx} className='card card-body bg-light p-2'>
<strong>{spec.name}</strong>
{spec.options && spec.options.length > 0 && (
<table className='table-sm table-borderless mt-1 mb-0 table'>
<tbody>
{spec.options.map((opt) => (
<tr key={opt.key}>
<td style={{ width: '40%', color: '#666' }}>{opt.key}</td>
<td>{opt.value}</td>
</tr>
))}
</tbody>
</table>
)}
</div>
))}
<small className='text-muted'>
Changing HNSW indexing options is only supported in the Bulk Edit
dialog.
</small>
</div>
)
}

render() {
const { deleting, errorMsg, predicateQuery, updating } = this.state
const { predicate } = this.props
Expand All @@ -95,7 +157,16 @@ export default class PredicatePropertiesPanel extends React.Component {
onChangeQuery={(predicateQuery) =>
this.setState({ predicateQuery })
}
onChangeType={this.handleTypeChange}
/>
{this.renderIndexSpecs()}
{this.state.currentType === 'float32vector' &&
this.props.predicate.type !== 'float32vector' && (
<div className='alert alert-info'>
Creating HNSW-based vector indexes is only supported in the Bulk
Edit dialog.
</div>
)}
{!errorMsg ? null : (
<div className='alert alert-danger'>{errorMsg}</div>
)}
Expand Down
29 changes: 25 additions & 4 deletions client/src/components/schema/PredicatesTable.js
Original file line number Diff line number Diff line change
Expand Up @@ -99,19 +99,40 @@ export default function PredicatesTable({
return badges
}

const formatIndexSpecs = (indexSpecs) => {
// Just show index names in the table column (e.g., "hnsw")
// Full details are shown in the Properties pane
return indexSpecs.map((spec) => spec.name).join(', ')
}

const createPredicateRow = (predicate, index) => {
const badges = getBadges(predicate)

let tokenizers = ''
if (predicate.index) {
predicate.tokenizer.sort()
tokenizers = predicate.tokenizer.join(', ')
// Show tokenizers if indexed, or if float32vector type with index_specs/tokenizers
const hasIndexSpecs =
predicate.index_specs && predicate.index_specs.length > 0
const hasTokenizers = predicate.tokenizer && predicate.tokenizer.length > 0
if (
predicate.index ||
(predicate.type === 'float32vector' && (hasIndexSpecs || hasTokenizers))
) {
if (hasIndexSpecs) {
// Use structured index_specs when available (float32vector)
tokenizers = formatIndexSpecs(predicate.index_specs)
} else {
predicate.tokenizer.sort()
tokenizers = predicate.tokenizer.join(', ')
}
}

if (badges.length) {
const badgesText = badges.map((b) => b.title).join(' ')
const sortkey = `${tokenizers} ${badgesText}`
const title = predicate.index ? sortkey : `Not indexed. ${badgesText}`
const hasIndex =
predicate.index ||
(predicate.type === 'float32vector' && (hasIndexSpecs || hasTokenizers))
const title = hasIndex ? sortkey : `Not indexed. ${badgesText}`
tokenizers = (
<div datasortkey={sortkey} title={title}>
<span style={{ display: 'inline-block' }}>{tokenizers || ' '}</span>
Expand Down
16 changes: 15 additions & 1 deletion client/src/components/schema/SchemaPredicateForm.js
Original file line number Diff line number Diff line change
Expand Up @@ -147,6 +147,10 @@ export default class SchemaPredicateForm extends React.Component {
predicate.upsert = false
predicate.unique = false
predicate.lang = false
// Notify parent of type change
if (this.props.onChangeType) {
this.props.onChangeType(predicate.type)
}
}),
)

Expand Down Expand Up @@ -340,17 +344,26 @@ export default class SchemaPredicateForm extends React.Component {
checked={predicate.list}
id='check-list'
label='list'
disabled={predicate.type === 'float32vector'}
onChange={this.handleListChange}
/>
)

if (predicate.type !== 'uid') {
// Check if original predicate (from props) had HNSW index
const originalPredicate = this.props.predicate
const hasHnswIndex =
predicate.type === 'float32vector' &&
originalPredicate.type === 'float32vector' &&
originalPredicate.tokenizer &&
originalPredicate.tokenizer.length > 0
indexInput = (
<Form.Check
type='checkbox'
checked={predicate.index}
checked={predicate.index || hasHnswIndex}
id='check-index'
label='index'
disabled={predicate.type === 'float32vector'}
onChange={this.handleIndexChange}
/>
)
Expand Down Expand Up @@ -463,6 +476,7 @@ export default class SchemaPredicateForm extends React.Component {
'bool',
'datetime',
'float',
'float32vector',
'geo',
'int',
'password',
Expand Down
15 changes: 14 additions & 1 deletion client/src/components/schema/SchemaPredicateModal.js
Original file line number Diff line number Diff line change
Expand Up @@ -20,9 +20,14 @@ export default class SchemaPredicateModal extends React.Component {
clickedSubmit: false,
errorMsg: '',
predicateQuery: null,
currentType: props.predicate?.type || 'int',
}
}

handleTypeChange = (newType) => {
this.setState({ currentType: newType })
}

async handleUpdatePredicate() {
const { executeQuery, onAfterUpdate } = this.props

Expand All @@ -46,7 +51,8 @@ export default class SchemaPredicateModal extends React.Component {

render() {
const { predicate, onCancel } = this.props
const { updating, clickedSubmit, errorMsg, predicateQuery } = this.state
const { updating, clickedSubmit, errorMsg, predicateQuery, currentType } =
this.state

const predicateForm = this.predicateForm.current
const canUpdate = predicateForm && !predicateForm.hasErrors()
Expand All @@ -65,7 +71,14 @@ export default class SchemaPredicateModal extends React.Component {
onChangeQuery={(predicateQuery) =>
this.setState({ predicateQuery })
}
onChangeType={this.handleTypeChange}
/>
{currentType === 'float32vector' && (
<div className='alert alert-info'>
Creating HNSW-based vector indexes is only supported in the Bulk
Edit dialog.
</div>
)}
{!errorMsg ? null : (
<div className='alert alert-danger'>{errorMsg}</div>
)}
Expand Down
31 changes: 29 additions & 2 deletions client/src/lib/dgraph-syntax.js
Original file line number Diff line number Diff line change
Expand Up @@ -11,19 +11,46 @@ export function isLatestVersion(ver) {
return ver === Unknown || ver.indexOf(LATEST_VERSION) === 0
}

// Reformat HNSW tokenizer from Dgraph's format to schema alter format
// Input: hnsw("maxLevels":"7","metric":"euclidean")
// Output: hnsw(maxLevels:"7", metric:"euclidean")
function formatHnswTokenizer(tokenizer) {
const match = tokenizer.match(/^hnsw\((.*)\)$/)
if (!match) {
return tokenizer
}
try {
const jsonStr = `{${match[1]}}`
const params = JSON.parse(jsonStr)
const options = Object.entries(params)
.map(([key, value]) => `${key}:"${value}"`)
.join(', ')
return `hnsw(${options})`
} catch {
return tokenizer
}
}

export function getPredicateTypeString(predicate) {
let type = predicate.type
const lang = type === 'string' && predicate.lang ? '@lang' : ''
if (predicate.list) {
type = '[' + type + ']'
}

const hasIndex = !!predicate.index
// For float32vector, check for tokenizers even if index flag isn't set
const hasTokenizers = predicate.tokenizer && predicate.tokenizer.length > 0
const hasIndex =
!!predicate.index || (predicate.type === 'float32vector' && hasTokenizers)
let tokenizers = ''
let upsert = ''
let unique = ''
if (hasIndex) {
tokenizers = predicate.tokenizer.join(', ')
// Format HNSW tokenizers to use unquoted keys for Dgraph schema alter
const formattedTokenizers = predicate.tokenizer.map((tok) =>
tok.startsWith('hnsw(') ? formatHnswTokenizer(tok) : tok,
)
tokenizers = formattedTokenizers.join(', ')
upsert = predicate.upsert ? '@upsert' : ''
unique = predicate.unique ? '@unique' : ''
}
Expand Down